diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d66d2487ce..d5bece232a 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -147,7 +147,10 @@ import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; import { MessagesTimeline } from "./chat/MessagesTimeline"; import { ChatHeader } from "./chat/ChatHeader"; -import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; +import { + getExpandedImagePreviewIdentityKey, + type ExpandedImagePreview, +} from "./chat/ExpandedImagePreview"; import { NoActiveThreadState } from "./NoActiveThreadState"; import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; @@ -3773,7 +3776,11 @@ export default function ChatView(props: ChatViewProps) { ) : null} {expandedImage && ( - + )} ); diff --git a/apps/web/src/components/chat/ExpandedImageDialog.tsx b/apps/web/src/components/chat/ExpandedImageDialog.tsx index 10031b48cd..8437f3c9c5 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,65 @@ 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]); - - 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 useExpandedImageDialogKeyboardShortcuts(input: { + readonly imageCount: number; + readonly onClose: () => void; + readonly onNavigate: (direction: -1 | 1) => void; +}) { + const close = useEffectEvent(input.onClose); + const navigate = useEffectEvent(input.onNavigate); useEffect(() => { const onKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); - onClose(); + close(); return; } - if (preview.images.length <= 1) return; + if (input.imageCount <= 1) return; if (event.key === "ArrowLeft") { event.preventDefault(); event.stopPropagation(); - navigateImage(-1); + navigate(-1); return; } if (event.key !== "ArrowRight") return; event.preventDefault(); event.stopPropagation(); - navigateImage(1); + navigate(1); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [navigateImage, onClose, preview.images.length]); + }, [close, input.imageCount, navigate]); +} + +export const ExpandedImageDialog = memo(function ExpandedImageDialog({ + preview, + onClose, +}: ExpandedImageDialogProps) { + const [navigationOffset, setNavigationOffset] = useState(0); + const imageCount = preview.images.length; + const activeIndex = + imageCount > 0 ? (preview.index + navigationOffset + imageCount) % imageCount : preview.index; + + const navigateImage = useCallback( + (direction: -1 | 1) => { + setNavigationOffset((offset) => { + if (imageCount <= 1) return offset; + return offset + direction; + }); + }, + [imageCount], + ); + + useExpandedImageDialogKeyboardShortcuts({ + imageCount, + onClose, + onNavigate: navigateImage, + }); - const item = preview.images[preview.index]; + const item = preview.images[activeIndex]; if (!item) return null; return ( @@ -69,7 +82,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ aria-label="Close image preview" onClick={onClose} /> - {preview.images.length > 1 && ( + {imageCount > 1 && (