diff --git a/apps/web/src/components/chat/ExpandedImageDialog.tsx b/apps/web/src/components/chat/ExpandedImageDialog.tsx index 10031b48cde..75a117b97f8 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 { 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,78 @@ interface ExpandedImageDialogProps { onClose: () => void; } -export const ExpandedImageDialog = memo(function ExpandedImageDialog({ - preview: initialPreview, - onClose, -}: ExpandedImageDialogProps) { - const [preview, setPreview] = useState(initialPreview); - - // Sync when the parent hands us a new preview reference. - useEffect(() => { - setPreview(initialPreview); - }, [initialPreview]); +export function ExpandedImageDialog({ preview, onClose }: ExpandedImageDialogProps) { + return ( + + ); +} - 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 }; - }); - }, []); +function getPreviewResetKey(preview: ExpandedImagePreview): string { + return [ + preview.index, + preview.images.length, + ...preview.images.map((image) => `${image.src}\u0000${image.name}`), + ].join("\u0001"); +} - 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: { + readonly canNavigate: boolean; + readonly onClose: () => void; + readonly onNext: () => void; + readonly onPrevious: () => void; +}) { + const handleKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + input.onClose(); + return; + } + if (!input.canNavigate) return; + if (event.key === "ArrowLeft") { event.preventDefault(); event.stopPropagation(); - navigateImage(1); - }; + input.onPrevious(); + return; + } + if (event.key !== "ArrowRight") return; + event.preventDefault(); + event.stopPropagation(); + input.onNext(); + }); + + useEffect(() => { + const onKeyDown = (event: globalThis.KeyboardEvent) => handleKeyDown(event); window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [navigateImage, onClose, preview.images.length]); + }, []); +} - const item = preview.images[preview.index]; +function ExpandedImageDialogContent({ preview, onClose }: ExpandedImageDialogProps) { + const [indexOffset, setIndexOffset] = useState(0); + const images = preview.images; + const canNavigate = images.length > 1; + const currentIndex = canNavigate + ? (preview.index + indexOffset + images.length) % images.length + : preview.index; + + const navigateImage = (direction: -1 | 1) => { + if (!canNavigate) return; + setIndexOffset((existingOffset) => existingOffset + direction); + }; + + useExpandedImageKeyboardShortcuts({ + canNavigate, + onClose, + onNext: () => navigateImage(1), + onPrevious: () => navigateImage(-1), + }); + + const item = images[currentIndex]; if (!item) return null; return ( @@ -69,7 +95,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ aria-label="Close image preview" onClick={onClose} /> - {preview.images.length > 1 && ( + {canNavigate && (