feat: snappier timeline — instant discard, progressive load, restore preview origin

- Discard yanks photos from the grid optimistically (cache strip + active
  cursor advance) instead of waiting for the mutation round-trip; wired
  through the X hotkey, RightSidebar bulk discard, LeftSidebar discard
  drop, and DiscardActionBar restore/delete.
- usePhotosQuery resolves on the first 500-photo page and streams the
  remaining pages into the cache in the background, so the first
  thumbnails paint immediately on large libraries.
- Closing preview restores the photo it was opened on (snapshot ref in
  PreviewView, written directly to the store) and Timeline scrolls that
  row back into view. Escape is handled on the dialog with
  stopPropagation so Timeline's window-level Esc handler doesn't wipe
  the restored selection.
- Preview overlay bumped to z-[1000] so it covers Leaflet map tiles,
  and the right sidebar no longer collapses during preview — both fix
  visible layout shifts on close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 00:37:33 +02:00
parent b2ebf401bb
commit f01b5ed77e
9 changed files with 290 additions and 38 deletions

View File

@@ -3,7 +3,7 @@ import { RotateCcw, Trash2 } from 'lucide-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { usePhotoStore } from '../../store/photoStore'
import { useFilterStore } from '../../store/filterStore'
import { usePhotosQuery } from '../../hooks/usePhotosQuery'
import { usePhotosQuery, stripPhotosFromCache } from '../../hooks/usePhotosQuery'
import { discard as discardApi, photos as photosApi } from '../../services/api'
import { toast } from '../ToastContainer'
import { ConfirmDialog } from '../dialogs/ConfirmDialog'
@@ -27,6 +27,14 @@ export function DiscardActionBar() {
const restoreMutation = useMutation({
mutationFn: (ids: string[]) => discardApi.restore(ids),
// Pull the restored ids out of the discard view immediately. The
// user is sitting on flag=discarded so they should disappear from
// sight the moment the click lands; the onSuccess invalidate still
// reconciles with server truth shortly after.
onMutate: (ids) => {
usePhotoStore.getState().removePhotosFromTimeline(ids)
stripPhotosFromCache(queryClient, ids)
},
onSuccess: (_, ids) => {
registerUndoable(
`Restored ${ids.length} photo${ids.length === 1 ? '' : 's'}`,
@@ -45,6 +53,10 @@ export function DiscardActionBar() {
const deleteSelectedMutation = useMutation({
mutationFn: (ids: string[]) => discardApi.deletePermanent(ids),
onMutate: (ids) => {
usePhotoStore.getState().removePhotosFromTimeline(ids)
stripPhotosFromCache(queryClient, ids)
},
onSuccess: (data: any) => {
const count = data?.deleted ?? 0
const errors = data?.file_errors ?? 0