diff --git a/apps/web/src/components/chat/ExpandedImageDialog.tsx b/apps/web/src/components/chat/ExpandedImageDialog.tsx index 10031b48cde..8f856466df4 100644 --- a/apps/web/src/components/chat/ExpandedImageDialog.tsx +++ b/apps/web/src/components/chat/ExpandedImageDialog.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useState } from "react"; +import { memo, useCallback, useEffect, useEffectEvent, useState } from "react"; import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react"; import { Button } from "../ui/button"; import type { ExpandedImagePreview } from "./ExpandedImagePreview"; @@ -8,52 +8,82 @@ interface ExpandedImageDialogProps { onClose: () => void; } -export const ExpandedImageDialog = memo(function ExpandedImageDialog({ - preview: initialPreview, - onClose, -}: ExpandedImageDialogProps) { - const [preview, setPreview] = useState(initialPreview); +function useExpandedImagePreviewNavigation(sourcePreview: ExpandedImagePreview) { + const [navigationState, setNavigationState] = useState(() => ({ + sourcePreview, + index: sourcePreview.index, + })); + const index = + navigationState.sourcePreview === sourcePreview ? navigationState.index : sourcePreview.index; - // Sync when the parent hands us a new preview reference. - useEffect(() => { - setPreview(initialPreview); - }, [initialPreview]); + const navigateImage = useCallback( + (direction: -1 | 1) => { + setNavigationState((existing) => { + if (sourcePreview.images.length <= 1) return existing; + const currentIndex = + existing.sourcePreview === sourcePreview ? existing.index : sourcePreview.index; + const nextIndex = + (currentIndex + direction + sourcePreview.images.length) % sourcePreview.images.length; + if (nextIndex === currentIndex && existing.sourcePreview === sourcePreview) { + return existing; + } + return { sourcePreview, index: nextIndex }; + }); + }, + [sourcePreview], + ); - const navigateImage = useCallback((direction: -1 | 1) => { - setPreview((existing) => { - if (existing.images.length <= 1) return existing; - const nextIndex = - (existing.index + direction + existing.images.length) % existing.images.length; - if (nextIndex === existing.index) return existing; - return { ...existing, index: nextIndex }; - }); - }, []); + return { + images: sourcePreview.images, + index, + navigateImage, + }; +} - useEffect(() => { - const onKeyDown = (event: globalThis.KeyboardEvent) => { - if (event.key === "Escape") { - event.preventDefault(); - event.stopPropagation(); - onClose(); - return; - } - if (preview.images.length <= 1) return; - if (event.key === "ArrowLeft") { - event.preventDefault(); - event.stopPropagation(); - navigateImage(-1); - return; - } - if (event.key !== "ArrowRight") return; +function useExpandedImageKeyboardShortcuts(input: { + imageCount: number; + navigateImage: (direction: -1 | 1) => void; + onClose: () => void; +}) { + const onKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + input.onClose(); + return; + } + if (input.imageCount <= 1) return; + if (event.key === "ArrowLeft") { event.preventDefault(); event.stopPropagation(); - navigateImage(1); - }; + input.navigateImage(-1); + return; + } + if (event.key !== "ArrowRight") return; + event.preventDefault(); + event.stopPropagation(); + input.navigateImage(1); + }); + + useEffect(() => { window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [navigateImage, onClose, preview.images.length]); + }, []); +} + +export const ExpandedImageDialog = memo(function ExpandedImageDialog({ + preview: initialPreview, + onClose, +}: ExpandedImageDialogProps) { + const { images, index, navigateImage } = useExpandedImagePreviewNavigation(initialPreview); + + useExpandedImageKeyboardShortcuts({ + imageCount: images.length, + navigateImage, + onClose, + }); - const item = preview.images[preview.index]; + const item = images[index]; if (!item) return null; return ( @@ -69,7 +99,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ aria-label="Close image preview" onClick={onClose} /> - {preview.images.length > 1 && ( + {images.length > 1 && (