diff --git a/frontend/src/components/preview/PreviewView.tsx b/frontend/src/components/preview/PreviewView.tsx index 90e7ed2..86c0347 100644 --- a/frontend/src/components/preview/PreviewView.tsx +++ b/frontend/src/components/preview/PreviewView.tsx @@ -45,22 +45,38 @@ export function PreviewView() { const safeIndex = currentIndex < 0 ? 0 : currentIndex const currentPhoto: Photo | undefined = photos[safeIndex] + // Keep the latest photos array + active id in a ref so the keyboard + // handlers ALWAYS read the freshest state. Without this, react-hotkeys- + // hook can fire a closure that captured an older photos array (e.g. + // the empty initial render before visiblePhotoIds was applied) and + // arrow nav lands on the wrong photo or no-ops. + const navRef = useRef({ photos, activePhotoId }) + navRef.current = { photos, activePhotoId } + const goPrev = useCallback(() => { - if (photos.length === 0) return - const next = Math.max(0, safeIndex - 1) - setActivePhoto(photos[next].id) - }, [photos, safeIndex, setActivePhoto]) + const { photos: ps, activePhotoId: aid } = navRef.current + if (ps.length === 0) return + const idx = aid ? ps.findIndex((p) => p.id === aid) : 0 + const safe = idx < 0 ? 0 : idx + const next = Math.max(0, safe - 1) + setActivePhoto(ps[next].id) + }, [setActivePhoto]) const goNext = useCallback(() => { - if (photos.length === 0) return - const next = Math.min(photos.length - 1, safeIndex + 1) - setActivePhoto(photos[next].id) - }, [photos, safeIndex, setActivePhoto]) + const { photos: ps, activePhotoId: aid } = navRef.current + if (ps.length === 0) return + const idx = aid ? ps.findIndex((p) => p.id === aid) : 0 + const safe = idx < 0 ? 0 : idx + const next = Math.min(ps.length - 1, safe + 1) + setActivePhoto(ps[next].id) + }, [setActivePhoto]) // Preview-scoped hotkeys: only mounted while PreviewView is rendered. + // The handlers themselves are stable (refs internally) so the deps + // array stays empty — useHotkeys won't have to re-bind on every render. useHotkeys('escape', closePreview, { preventDefault: true }) - useHotkeys('left', goPrev, { preventDefault: true }, [goPrev]) - useHotkeys('right', goNext, { preventDefault: true }, [goNext]) + useHotkeys('left', goPrev, { preventDefault: true }) + useHotkeys('right', goNext, { preventDefault: true }) useHotkeys('i', () => setInfoPanelOpen((v) => !v), { preventDefault: true }) // Preload the immediate neighbors so arrow nav feels instant. Skip videos