fix: pass visible sequence to openPreview from the click site
Previously the visible photo order was published only via a passive useEffect on Timeline, which had a timing race: arrow nav in preview could read a stale or empty sequence and fall back to the raw API order, breaking visual order navigation in tag mode and after filter changes. Fix: openPreview now accepts an optional visibleSequence parameter, and Timeline's onDoubleClick passes the freshly-computed flat sequence directly. The store action adopts that sequence as the authoritative visiblePhotoIds for the preview session, falling back to the most-recently-published one for paths that don't have a click site (e.g. the global Space hotkey). The Timeline still publishes via useEffect for the Space-hotkey fallback path, but the click path no longer depends on it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -297,20 +297,27 @@ export function Timeline() {
|
|||||||
[items]
|
[items]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Publish the flat visible-order id sequence to the photo store so
|
// Flat visible-order id sequence — exactly the order the user reads
|
||||||
// PreviewView arrow nav (and the filmstrip) walks the same order the
|
// off the grid (top-to-bottom, left-to-right within each row).
|
||||||
// user sees in the grid. Includes duplicates from tag grouping —
|
// Includes duplicates from tag-grouping; landing on the same photo's
|
||||||
// landing on the same photo's "second" appearance in the next tag
|
// "second" appearance in the next tag bucket is the right behavior
|
||||||
// bucket is the right behavior in tag mode.
|
// in tag mode.
|
||||||
useEffect(() => {
|
const visibleSequence = useMemo(() => {
|
||||||
const ids: string[] = []
|
const ids: string[] = []
|
||||||
for (const row of photoRows) {
|
for (const row of photoRows) {
|
||||||
for (const cell of row.cells) {
|
for (const cell of row.cells) {
|
||||||
ids.push(cell.photo.id)
|
ids.push(cell.photo.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setVisiblePhotoIds(ids)
|
return ids
|
||||||
}, [photoRows, setVisiblePhotoIds])
|
}, [photoRows])
|
||||||
|
|
||||||
|
// Publish to the photo store so PreviewView's arrow nav and filmstrip
|
||||||
|
// can walk the same order even when opened from a non-click path
|
||||||
|
// (e.g. the global Space hotkey).
|
||||||
|
useEffect(() => {
|
||||||
|
setVisiblePhotoIds(visibleSequence)
|
||||||
|
}, [visibleSequence, setVisiblePhotoIds])
|
||||||
|
|
||||||
// Locate the active photo in the visual grid. Returns the FIRST
|
// Locate the active photo in the visual grid. Returns the FIRST
|
||||||
// (rowIndex, colIndex) where its id appears, since a tag-grouped view
|
// (rowIndex, colIndex) where its id appears, since a tag-grouped view
|
||||||
@@ -518,7 +525,7 @@ export function Timeline() {
|
|||||||
selectPhoto(photo.id, globalIndex)
|
selectPhoto(photo.id, globalIndex)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => openPreview(photo.id)}
|
onDoubleClick={() => openPreview(photo.id, visibleSequence)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ interface PhotoStore {
|
|||||||
setActivePhoto: (id: string | null) => void
|
setActivePhoto: (id: string | null) => void
|
||||||
setViewMode: (mode: ViewMode) => void
|
setViewMode: (mode: ViewMode) => void
|
||||||
setVisiblePhotoIds: (ids: string[]) => void
|
setVisiblePhotoIds: (ids: string[]) => void
|
||||||
openPreview: (id: string) => void
|
/** Open preview on a specific photo. The visibleSequence (optional)
|
||||||
|
* is the ordered list of photo ids the user currently sees in the
|
||||||
|
* timeline; passing it from the click site avoids a race where the
|
||||||
|
* passive Timeline publisher hasn't updated yet. */
|
||||||
|
openPreview: (id: string, visibleSequence?: string[]) => void
|
||||||
closePreview: () => void
|
closePreview: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +103,19 @@ export const usePhotoStore = create<PhotoStore>((set) => ({
|
|||||||
return { visiblePhotoIds }
|
return { visiblePhotoIds }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
openPreview: (id) => set({ viewMode: 'preview', activePhotoId: id }),
|
openPreview: (id, visibleSequence) =>
|
||||||
|
set((s) => ({
|
||||||
|
viewMode: 'preview',
|
||||||
|
activePhotoId: id,
|
||||||
|
// Adopt the caller-provided sequence when they pass one. Falls
|
||||||
|
// back to whatever Timeline most recently published, which is
|
||||||
|
// correct for paths like the global Space hotkey that don't have
|
||||||
|
// a click site to compute the sequence from.
|
||||||
|
visiblePhotoIds:
|
||||||
|
visibleSequence && visibleSequence.length > 0
|
||||||
|
? visibleSequence
|
||||||
|
: s.visiblePhotoIds,
|
||||||
|
})),
|
||||||
|
|
||||||
closePreview: () => set({ viewMode: 'grid' }),
|
closePreview: () => set({ viewMode: 'grid' }),
|
||||||
}))
|
}))
|
||||||
Reference in New Issue
Block a user