From 676de4c1d92e65d4308a79f8a43b90457fe89b9c Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 10:19:39 +0200 Subject: [PATCH] refactor(files): share the media lightbox logic between desktop and phone useMediaLightbox holds the signed image address of the file on screen, paging by button, arrow keys and swipe, and Escape to close. The desktop lightbox keeps its own index and thumbnail strip, the phone lightbox stays controlled by the files tab; both draw their own overlay over the hook. --- .../Files/FileManagerImageLightbox.tsx | 49 ++----- .../components/Files/useMediaLightbox.test.ts | 131 ++++++++++++++++++ .../src/components/Files/useMediaLightbox.ts | 78 +++++++++++ .../screens/trip/tabs/MFileLightbox.tsx | 57 ++------ 4 files changed, 229 insertions(+), 86 deletions(-) create mode 100644 client/src/components/Files/useMediaLightbox.test.ts create mode 100644 client/src/components/Files/useMediaLightbox.ts diff --git a/client/src/components/Files/FileManagerImageLightbox.tsx b/client/src/components/Files/FileManagerImageLightbox.tsx index 9ad8a2646..0b0dfd426 100644 --- a/client/src/components/Files/FileManagerImageLightbox.tsx +++ b/client/src/components/Files/FileManagerImageLightbox.tsx @@ -6,6 +6,7 @@ import type { TripFile } from '../../types' import { getAuthUrl } from '../../api/authUrl' import { openFile as openFileUrl } from '../../utils/fileDownload' import { triggerDownload, isVideo } from './FileManager.helpers' +import { useMediaLightbox } from './useMediaLightbox' import VideoPlayer from '../Journey/VideoPlayerLazy' import { Tooltip } from '../shared/Tooltip' @@ -22,41 +23,15 @@ interface ImageLightboxProps { export function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxProps) { const { t } = useTranslation() const [index, setIndex] = useState(initialIndex) - const [imgSrc, setImgSrc] = useState('') - const [touchStart, setTouchStart] = useState(null) - const file = files[index] - - const fileIsVideo = isVideo(file?.mime_type) - - useEffect(() => { - setImgSrc('') - // Images use a one-shot signed URL; a video must use the plain same-origin - // URL (cookie auth) so its many Range requests all authenticate (#823). - if (!file || isVideo(file.mime_type)) return - // Arrowing through the gallery leaves several mints in flight; only the one for - // the file still on screen may paint. - let current = true - getAuthUrl(file.url, 'download').then(u => { if (current) setImgSrc(u) }).catch(() => {}) - return () => { current = false } - }, [file?.url, file?.mime_type]) - - const goPrev = () => setIndex(i => Math.max(0, i - 1)) - const goNext = () => setIndex(i => Math.min(files.length - 1, i + 1)) - - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose() - if (e.key === 'ArrowLeft') goPrev() - if (e.key === 'ArrowRight') goNext() - } - window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) - }, [onClose]) + const { file, imgSrc, fileIsVideo, hasPrev, hasNext, goPrev, goNext, onTouchStart, onTouchEnd } = useMediaLightbox({ + files, + index, + onIndexChange: setIndex, + onClose, + }) if (!file) return null - const hasPrev = index > 0 - const hasNext = index < files.length - 1 const navBtn = (side: 'left' | 'right', onClick: () => void, show: boolean): React.ReactNode => show ? (