diff --git a/app/javascript/mastodon/features/ui/components/media_modal.tsx b/app/javascript/mastodon/features/ui/components/media_modal.tsx index 7ba2fb7b732..daa0aebeb3f 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.tsx @@ -46,6 +46,8 @@ interface MediaModalProps { volume?: number; } +const MIN_SWIPE_DISTANCE = 400; + export const MediaModal: FC = forwardRef< HTMLDivElement, MediaModalProps @@ -69,21 +71,30 @@ export const MediaModal: FC = forwardRef< const [zoomedIn, setZoomedIn] = useState(false); const currentMedia = media.get(index); + const [wrapperStyles, api] = useSpring(() => ({ + x: `-${index * 100}%`, + })); + const handleChangeIndex = useCallback( - (newIndex: number) => { + (newIndex: number, animate = false) => { if (newIndex < 0) { newIndex = media.size + newIndex; + } else if (newIndex >= media.size) { + newIndex = newIndex % media.size; } - setIndex(newIndex % media.size); + setIndex(newIndex); setZoomedIn(false); + if (animate) { + void api.start({ x: `-${newIndex * 100}%` }); + } }, - [media.size], + [api, media.size], ); const handlePrevClick = useCallback(() => { - handleChangeIndex(index - 1); + handleChangeIndex(index - 1, true); }, [handleChangeIndex, index]); const handleNextClick = useCallback(() => { - handleChangeIndex(index + 1); + handleChangeIndex(index + 1, true); }, [handleChangeIndex, index]); const handleKeyDown = useCallback( @@ -101,6 +112,25 @@ export const MediaModal: FC = forwardRef< [handleNextClick, handlePrevClick], ); + const bind = useDrag( + ({ active, movement: [mx], direction: [xDir], cancel }) => { + // 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(); + } + // 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 } }, + ); + useEffect(() => { window.addEventListener('keydown', handleKeyDown, false); @@ -145,17 +175,6 @@ export const MediaModal: FC = forwardRef< setZoomedIn((prev) => !prev); }, []); - const wrapperStyles = useSpring({ - x: `-${index * 100}%`, - }); - const bind = useDrag( - ({ swipe: [swipeX] }) => { - if (swipeX === 0) return; - handleChangeIndex(index + swipeX * -1); // Invert swipe as swiping left loads the next slide. - }, - { pointer: { capture: false } }, - ); - const [navigationHidden, setNavigationHidden] = useState(false); const handleToggleNavigation = useCallback(() => { setNavigationHidden((prev) => !prev);