|
|
|
|
@ -1,8 +1,6 @@
|
|
|
|
|
import { animated, useSpring } from '@react-spring/web';
|
|
|
|
|
import { Link } from '@tanstack/react-router';
|
|
|
|
|
import { useDrag } from '@use-gesture/react';
|
|
|
|
|
import clsx from 'clsx';
|
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
|
import React, { type RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
|
|
|
|
|
|
|
|
|
import { fetchStatusWithContext } from '@/actions/statuses';
|
|
|
|
|
@ -12,6 +10,7 @@ import StatusActionBar from '@/components/status-action-bar';
|
|
|
|
|
import HStack from '@/components/ui/hstack';
|
|
|
|
|
import Icon from '@/components/ui/icon';
|
|
|
|
|
import IconButton from '@/components/ui/icon-button';
|
|
|
|
|
import Stack from '@/components/ui/stack';
|
|
|
|
|
import Audio from '@/features/audio';
|
|
|
|
|
import PlaceholderStatus from '@/features/placeholder/components/placeholder-status';
|
|
|
|
|
import Thread from '@/features/status/components/thread';
|
|
|
|
|
@ -25,8 +24,6 @@ import { makeGetStatus } from '@/selectors';
|
|
|
|
|
import type { BaseModalProps } from '@/features/ui/components/modal-root';
|
|
|
|
|
import type { MediaAttachment } from 'pl-api';
|
|
|
|
|
|
|
|
|
|
const MIN_SWIPE_DISTANCE = 400;
|
|
|
|
|
|
|
|
|
|
const messages = defineMessages({
|
|
|
|
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
|
|
|
expand: { id: 'lightbox.expand', defaultMessage: 'Expand' },
|
|
|
|
|
@ -66,93 +63,95 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
const [navigationHidden, setNavigationHidden] = useState(false);
|
|
|
|
|
const [isFullScreen, setIsFullScreen] = useState(!status);
|
|
|
|
|
|
|
|
|
|
const [wrapperStyles, api] = useSpring(() => ({
|
|
|
|
|
x: `-${index * 100}%`,
|
|
|
|
|
}), [index]);
|
|
|
|
|
const handleChangeIndex = useCallback(
|
|
|
|
|
(newIndex: number) => {
|
|
|
|
|
if (newIndex < 0) {
|
|
|
|
|
newIndex = media.length + newIndex;
|
|
|
|
|
} else if (newIndex >= media.length) {
|
|
|
|
|
newIndex = newIndex % media.length;
|
|
|
|
|
}
|
|
|
|
|
setIndex(newIndex);
|
|
|
|
|
setZoomedIn(false);
|
|
|
|
|
},
|
|
|
|
|
[media.length],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleChangeIndex = useCallback((newIndex: number, animate = false) => {
|
|
|
|
|
if (newIndex < 0) {
|
|
|
|
|
newIndex = media.length + newIndex;
|
|
|
|
|
} else if (newIndex >= media.length) {
|
|
|
|
|
newIndex = newIndex % media.length;
|
|
|
|
|
}
|
|
|
|
|
setIndex(newIndex);
|
|
|
|
|
setZoomedIn(false);
|
|
|
|
|
if (animate) {
|
|
|
|
|
void api.start({ x: `calc(-${newIndex * 100}% + 0px)` });
|
|
|
|
|
}
|
|
|
|
|
}, [api, media.length]);
|
|
|
|
|
const handlePrevClick = useCallback(() => {
|
|
|
|
|
handleChangeIndex(index - 1, true);
|
|
|
|
|
}, [handleChangeIndex, index]);
|
|
|
|
|
const handleNextClick = useCallback(() => {
|
|
|
|
|
handleChangeIndex(index + 1, true);
|
|
|
|
|
const handlePrevClick = useCallback((e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleChangeIndex(index - 1);
|
|
|
|
|
}, [handleChangeIndex, index]);
|
|
|
|
|
|
|
|
|
|
// const [viewportDimensions, setViewportDimensions] = useState<{
|
|
|
|
|
// width: number;
|
|
|
|
|
// height: number;
|
|
|
|
|
// }>({ width: 0, height: 0 });
|
|
|
|
|
const handleNextClick = useCallback((e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleChangeIndex(index + 1);
|
|
|
|
|
}, [handleChangeIndex, index]);
|
|
|
|
|
|
|
|
|
|
// const handleRef: RefCallback<HTMLDivElement> = useCallback((ele) => {
|
|
|
|
|
// if (ele?.clientWidth && ele.clientHeight) {
|
|
|
|
|
// setViewportDimensions({
|
|
|
|
|
// width: ele.clientWidth,
|
|
|
|
|
// height: ele.clientHeight,
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
// }, []);
|
|
|
|
|
const [viewportDimensions, setViewportDimensions] = useState<{
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
}>({ width: 0, height: 0 });
|
|
|
|
|
|
|
|
|
|
const handleRef: RefCallback<HTMLDivElement> = useCallback((ele) => {
|
|
|
|
|
if (ele?.clientWidth && ele.clientHeight) {
|
|
|
|
|
setViewportDimensions({
|
|
|
|
|
width: ele.clientWidth,
|
|
|
|
|
height: ele.clientHeight,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const hasMultipleImages = media.length > 1;
|
|
|
|
|
|
|
|
|
|
const navigationHiddenClassName = navigationHidden ? 'pointer-events-none opacity-0' : '';
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
|
|
|
switch (e.key) {
|
|
|
|
|
case 'ArrowLeft':
|
|
|
|
|
handlePrevClick();
|
|
|
|
|
handleChangeIndex(index - 1);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
break;
|
|
|
|
|
case 'ArrowRight':
|
|
|
|
|
handleNextClick();
|
|
|
|
|
handleChangeIndex(index + 1);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [handleChangeIndex, index]);
|
|
|
|
|
|
|
|
|
|
const bind = useDrag(
|
|
|
|
|
({ active, movement: [mx], direction: [xDir], cancel, event }) => {
|
|
|
|
|
// Disable swipe when zoomed in.
|
|
|
|
|
if (zoomedIn) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Touch swipe handling
|
|
|
|
|
const touchStartX = useRef<number | null>(null);
|
|
|
|
|
const touchStartY = useRef<number | null>(null);
|
|
|
|
|
const carouselRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
// Disable swipe when interacting with video/audio controls or other interactive elements
|
|
|
|
|
const target = event?.target as HTMLElement | null;
|
|
|
|
|
if (target) {
|
|
|
|
|
const interactiveParent = target.closest('.video-player__controls, button');
|
|
|
|
|
if (interactiveParent) {
|
|
|
|
|
cancel();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
|
|
|
|
if (zoomedIn) return;
|
|
|
|
|
touchStartX.current = e.touches[0].clientX;
|
|
|
|
|
touchStartY.current = e.touches[0].clientY;
|
|
|
|
|
}, [zoomedIn]);
|
|
|
|
|
|
|
|
|
|
// If dragging and swipe distance is enough, change the index.
|
|
|
|
|
if (
|
|
|
|
|
active &&
|
|
|
|
|
Math.abs(mx) > Math.min(window.innerWidth / 4, MIN_SWIPE_DISTANCE)
|
|
|
|
|
) {
|
|
|
|
|
handleChangeIndex(index - xDir);
|
|
|
|
|
cancel();
|
|
|
|
|
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
|
|
|
|
|
if (zoomedIn || touchStartX.current === null || touchStartY.current === null) return;
|
|
|
|
|
|
|
|
|
|
const touchEndX = e.changedTouches[0].clientX;
|
|
|
|
|
const touchEndY = e.changedTouches[0].clientY;
|
|
|
|
|
const deltaX = touchEndX - touchStartX.current;
|
|
|
|
|
const deltaY = touchEndY - touchStartY.current;
|
|
|
|
|
|
|
|
|
|
// Only trigger if horizontal swipe is greater than vertical (to avoid conflicts with scroll)
|
|
|
|
|
// and if the swipe distance is significant enough
|
|
|
|
|
const minSwipeDistance = 50;
|
|
|
|
|
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > minSwipeDistance) {
|
|
|
|
|
if (deltaX > 0) {
|
|
|
|
|
handleChangeIndex(index - 1); // Swipe right = previous
|
|
|
|
|
} else {
|
|
|
|
|
handleChangeIndex(index + 1); // Swipe left = next
|
|
|
|
|
}
|
|
|
|
|
// Set the x position via calc to ensure proper centering regardless of screen size.
|
|
|
|
|
const x = active ? mx : 0;
|
|
|
|
|
void api.start({ x: `calc(-${index * 100}% + ${x}px)` });
|
|
|
|
|
},
|
|
|
|
|
{ pointer: { capture: false } },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
touchStartX.current = null;
|
|
|
|
|
touchStartY.current = null;
|
|
|
|
|
}, [zoomedIn, handleChangeIndex, index]);
|
|
|
|
|
|
|
|
|
|
const handleDownload = () => {
|
|
|
|
|
const mediaItem = hasMultipleImages ? media[index as number] : media[0];
|
|
|
|
|
@ -163,17 +162,17 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
setNavigationHidden(value => !value && userTouching.matches);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// const currentMedia = media[index];
|
|
|
|
|
const currentMedia = media[index];
|
|
|
|
|
|
|
|
|
|
// const zoomable =
|
|
|
|
|
// currentMedia.type === 'image' && currentMedia.meta.original &&
|
|
|
|
|
// (currentMedia.meta.original.width > viewportDimensions.width || currentMedia.meta.original.height > viewportDimensions.height);
|
|
|
|
|
const zoomable =
|
|
|
|
|
currentMedia.type === 'image' && currentMedia.meta.original &&
|
|
|
|
|
(currentMedia.meta.original.width > viewportDimensions.width || currentMedia.meta.original.height > viewportDimensions.height);
|
|
|
|
|
|
|
|
|
|
const handleZoomClick = useCallback(() => {
|
|
|
|
|
setZoomedIn((prev) => !prev);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const content = useMemo(() => media.map((attachment, idx) => {
|
|
|
|
|
const content = useMemo(() => media.map((attachment, i) => {
|
|
|
|
|
let width: number | undefined, height: number | undefined;
|
|
|
|
|
if (attachment.type === 'image' || attachment.type === 'gifv' || attachment.type === 'video') {
|
|
|
|
|
width = (attachment.meta?.original?.width);
|
|
|
|
|
@ -186,65 +185,71 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
</Link>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const slideStyle = { width: `${100 / media.length}%` };
|
|
|
|
|
|
|
|
|
|
if (attachment.type === 'image') {
|
|
|
|
|
return (
|
|
|
|
|
<ZoomableImage
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
blurhash={attachment.blurhash || undefined}
|
|
|
|
|
width={width!}
|
|
|
|
|
height={height!}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
lang={props.lang}
|
|
|
|
|
key={attachment.url}
|
|
|
|
|
onClick={toggleNavigation}
|
|
|
|
|
onDoubleClick={handleZoomClick}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onZoomChange={setZoomedIn}
|
|
|
|
|
zoomedIn={zoomedIn && idx === index}
|
|
|
|
|
/>
|
|
|
|
|
<div key={attachment.url} className='h-full shrink-0' style={slideStyle}>
|
|
|
|
|
<ZoomableImage
|
|
|
|
|
blurhash={attachment.blurhash || undefined}
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
width={width!}
|
|
|
|
|
height={height!}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
lang={props.lang}
|
|
|
|
|
onClick={toggleNavigation}
|
|
|
|
|
onDoubleClick={handleZoomClick}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
onZoomChange={setZoomedIn}
|
|
|
|
|
zoomedIn={zoomedIn && i === index}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (attachment.type === 'video') {
|
|
|
|
|
return (
|
|
|
|
|
<Video
|
|
|
|
|
preview={attachment.preview_url}
|
|
|
|
|
blurhash={attachment.blurhash}
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
startTime={time}
|
|
|
|
|
detailed
|
|
|
|
|
autoFocus={idx === index}
|
|
|
|
|
link={link}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
key={attachment.url}
|
|
|
|
|
visible
|
|
|
|
|
/>
|
|
|
|
|
<div key={attachment.url} className='h-full shrink-0' style={slideStyle}>
|
|
|
|
|
<Video
|
|
|
|
|
preview={attachment.preview_url}
|
|
|
|
|
blurhash={attachment.blurhash}
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
startTime={time}
|
|
|
|
|
detailed
|
|
|
|
|
autoFocus={i === index}
|
|
|
|
|
link={link}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
visible
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (attachment.type === 'audio') {
|
|
|
|
|
return (
|
|
|
|
|
<Audio
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
poster={attachment.preview_url !== attachment.url ? attachment.preview_url : (status?.account.avatar_static) as string | undefined}
|
|
|
|
|
backgroundColor={attachment.meta.colors?.background as string | undefined}
|
|
|
|
|
foregroundColor={attachment.meta.colors?.foreground as string | undefined}
|
|
|
|
|
accentColor={attachment.meta.colors?.accent as string | undefined}
|
|
|
|
|
duration={attachment.meta.original?.duration || 0}
|
|
|
|
|
key={attachment.url}
|
|
|
|
|
/>
|
|
|
|
|
<div key={attachment.url} className='h-full shrink-0' style={slideStyle}>
|
|
|
|
|
<Audio
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
poster={attachment.preview_url !== attachment.url ? attachment.preview_url : (status?.account.avatar_static) as string | undefined}
|
|
|
|
|
backgroundColor={attachment.meta.colors?.background as string | undefined}
|
|
|
|
|
foregroundColor={attachment.meta.colors?.foreground as string | undefined}
|
|
|
|
|
accentColor={attachment.meta.colors?.accent as string | undefined}
|
|
|
|
|
duration={attachment.meta.original?.duration || 0}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (attachment.type === 'gifv') {
|
|
|
|
|
return (
|
|
|
|
|
<ExtendedVideoPlayer
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
muted
|
|
|
|
|
controls={false}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
key={attachment.preview_url}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
onClick={toggleNavigation}
|
|
|
|
|
/>
|
|
|
|
|
<div key={attachment.preview_url} className='h-full shrink-0' style={slideStyle}>
|
|
|
|
|
<ExtendedVideoPlayer
|
|
|
|
|
src={attachment.url}
|
|
|
|
|
muted
|
|
|
|
|
controls={false}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
alt={attachment.description}
|
|
|
|
|
onClick={toggleNavigation}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -287,27 +292,26 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={clsx('⁂-media-modal', { '⁂-media-modal--fullscreen': isFullScreen })} role='presentation'>
|
|
|
|
|
<div className='⁂-media-modal media-modal pointer-events-auto fixed inset-0 z-[9999] h-full bg-gray-900/90'>
|
|
|
|
|
<div
|
|
|
|
|
{...bind()}
|
|
|
|
|
onClick={handleClickOutside}
|
|
|
|
|
className='⁂-media-modal__content'
|
|
|
|
|
// ref={handleRef}
|
|
|
|
|
className='absolute inset-0'
|
|
|
|
|
role='presentation'
|
|
|
|
|
>
|
|
|
|
|
<animated.div
|
|
|
|
|
style={wrapperStyles}
|
|
|
|
|
className='⁂-media-modal__closer'
|
|
|
|
|
role='presentation'
|
|
|
|
|
onClick={() => onClose()}
|
|
|
|
|
<Stack
|
|
|
|
|
onClick={handleClickOutside}
|
|
|
|
|
className={
|
|
|
|
|
clsx('⁂-media-modal__content fixed inset-0 h-full grow transition-all', {
|
|
|
|
|
'xl:pr-96': !isFullScreen,
|
|
|
|
|
'xl:pr-0': isFullScreen,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
justifyContent='between'
|
|
|
|
|
ref={handleRef}
|
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</animated.div>
|
|
|
|
|
|
|
|
|
|
<div className='⁂-media-modal__navigation'>
|
|
|
|
|
<HStack
|
|
|
|
|
alignItems='center'
|
|
|
|
|
justifyContent='between'
|
|
|
|
|
className={clsx('pointer-events-auto z-10 flex-[0_0_60px] p-4 transition-opacity', navigationHiddenClassName)}
|
|
|
|
|
className={clsx('flex-[0_0_60px] p-4 transition-opacity', navigationHiddenClassName)}
|
|
|
|
|
>
|
|
|
|
|
<IconButton
|
|
|
|
|
title={intl.formatMessage(messages.close)}
|
|
|
|
|
@ -319,7 +323,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<HStack alignItems='center' space={2}>
|
|
|
|
|
{/* {zoomable && (
|
|
|
|
|
{zoomable && (
|
|
|
|
|
<IconButton
|
|
|
|
|
title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)}
|
|
|
|
|
src={zoomedIn ? require('@phosphor-icons/core/regular/magnifying-glass-minus.svg') : require('@phosphor-icons/core/regular/magnifying-glass-plus.svg')}
|
|
|
|
|
@ -328,7 +332,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
iconClassName='h-5 w-5'
|
|
|
|
|
onClick={handleZoomClick}
|
|
|
|
|
/>
|
|
|
|
|
)} */}
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<IconButton
|
|
|
|
|
title={intl.formatMessage(messages.download)}
|
|
|
|
|
@ -351,9 +355,13 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
)}
|
|
|
|
|
</HStack>
|
|
|
|
|
</HStack>
|
|
|
|
|
{hasMultipleImages && (
|
|
|
|
|
<HStack className='z-10 mx-5' justifyContent='between'>
|
|
|
|
|
<div className={clsx('pointer-events-auto z-10 flex h-fit items-center transition-opacity', navigationHiddenClassName)}>
|
|
|
|
|
|
|
|
|
|
{/* Height based on height of top/bottom bars */}
|
|
|
|
|
<div
|
|
|
|
|
className='relative h-[calc(100vh-120px)] w-full grow'
|
|
|
|
|
>
|
|
|
|
|
{hasMultipleImages && (
|
|
|
|
|
<div className={clsx('absolute inset-y-0 left-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
|
|
|
|
<button
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
|
|
|
|
|
@ -363,7 +371,28 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
<Icon src={require('@phosphor-icons/core/regular/arrow-left.svg')} className='size-5' />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={clsx('pointer-events-auto z-10 flex h-fit items-center transition-opacity', navigationHiddenClassName)}>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
ref={carouselRef}
|
|
|
|
|
className='absolute inset-0 overflow-hidden'
|
|
|
|
|
onTouchStart={handleTouchStart}
|
|
|
|
|
onTouchEnd={handleTouchEnd}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: `${media.length * 100}%`,
|
|
|
|
|
transform: `translateX(-${index * (100 / media.length)}%)`,
|
|
|
|
|
}}
|
|
|
|
|
className='flex h-full transition-transform duration-300 ease-out'
|
|
|
|
|
role='presentation'
|
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{hasMultipleImages && (
|
|
|
|
|
<div className={clsx('absolute inset-y-0 right-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
|
|
|
|
<button
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
className='flex size-10 items-center justify-center rounded-full bg-gray-900 text-white'
|
|
|
|
|
@ -373,12 +402,44 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
<Icon src={require('@phosphor-icons/core/regular/arrow-right.svg')} className='size-5' />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</HStack>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{hasMultipleImages && (
|
|
|
|
|
<div className={clsx('flex-none overflow-x-auto py-2 transition-opacity', navigationHiddenClassName)}>
|
|
|
|
|
<HStack
|
|
|
|
|
justifyContent='center'
|
|
|
|
|
space={2}
|
|
|
|
|
className='min-w-min px-4'
|
|
|
|
|
>
|
|
|
|
|
{media.map((attachment, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={attachment.id || i}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setIndex(i);
|
|
|
|
|
}}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'size-12 flex-none overflow-hidden rounded-lg border-2 transition-all hover:scale-105',
|
|
|
|
|
i === index ? 'border-white' : 'border-transparent opacity-60 hover:opacity-100',
|
|
|
|
|
)}
|
|
|
|
|
aria-label={`View image ${i + 1}`}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={attachment.preview_url || attachment.url}
|
|
|
|
|
alt={attachment.description || `Image ${i + 1}`}
|
|
|
|
|
className='size-full object-cover'
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</HStack>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{status ? (
|
|
|
|
|
|
|
|
|
|
{status && (
|
|
|
|
|
<HStack
|
|
|
|
|
justifyContent='center'
|
|
|
|
|
className={clsx('pointer-events-auto flex-[0_0_60px] transition-opacity', navigationHiddenClassName)}
|
|
|
|
|
className={clsx('flex-[0_0_60px] transition-opacity', navigationHiddenClassName)}
|
|
|
|
|
>
|
|
|
|
|
<StatusActionBar
|
|
|
|
|
status={status}
|
|
|
|
|
@ -386,26 +447,26 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
|
|
|
|
expandable
|
|
|
|
|
/>
|
|
|
|
|
</HStack>
|
|
|
|
|
) : <span />}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Stack>
|
|
|
|
|
|
|
|
|
|
{status && (
|
|
|
|
|
<div
|
|
|
|
|
className={
|
|
|
|
|
clsx('-right-96 hidden bg-white transition-all xl:fixed xl:inset-y-0 xl:right-0 xl:flex xl:w-96 xl:flex-col', {
|
|
|
|
|
'xl:!-right-96': isFullScreen,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Thread
|
|
|
|
|
status={status}
|
|
|
|
|
withMedia={false}
|
|
|
|
|
itemClassName='px-4'
|
|
|
|
|
isModal
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{status && (
|
|
|
|
|
<div
|
|
|
|
|
className={
|
|
|
|
|
clsx('-right-96 hidden bg-white transition-all xl:fixed xl:inset-y-0 xl:right-0 xl:flex xl:w-96 xl:flex-col', {
|
|
|
|
|
'xl:!-right-96': isFullScreen,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Thread
|
|
|
|
|
status={status}
|
|
|
|
|
withMedia={false}
|
|
|
|
|
itemClassName='px-4'
|
|
|
|
|
isModal
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|