- swipe sideways to browse (track follows the finger, FLIP settle),
flick up/down to vote Tinder-style with ghost fly-off and the next
photo zooming in behind; votes and navigation commit synchronously on
release — animations are pure decoration, so closing or keypresses
mid-animation can't drop or duplicate an action
- two fingers pinch-zoom the photo (springs back, never votes)
- voting the last photo fades the modal out over the gallery
- gesture engine extracted to useSwipe: per-move motion via direct DOM
writes (no React render per touchmove), named threshold constants
- touch devices: selection checkmarks always visible, nav arrows yield
to swiping, finger-sized vote/rating targets, safe-area padding
- expiry editor commits on blur/Enter with explicit clear button — no more
per-keystroke PATCHes transiently expiring live links
- shift-range walks the filtered view, anchors reset on clear, updaters
kept pure (StrictMode-safe)
- bulk delete surfaces errors, 404s on zero deletions, and shares one
delete+cleanup path with single and album delete (s3::photo_prefix is
the only prefix source)
- expiry end-of-day convention extracted; share rows update from the
PATCH response instead of reloading the list
- Gallery container always renders, restoring the simple observer effect
- shift-click selects ranges in the gallery; selection bar gains
'Delete N' with a single confirm (POST /api/photos/delete)
- PATCH /api/shares/{id}: allow_download and expiry editable in place,
token and client feedback preserved
- Gallery: re-attach ResizeObserver via callback ref — after a filter
with zero matches the gallery stayed blank at width 0
- verdicts table (per link, like ratings), PUT verdict endpoint, typed Verdict enum
- share page: thumbs up/down, verdict filter with counts, view-scoped selection bar
- lightbox: per-page shortcut table (P/X/U, 1-5/0, S), ? help overlay, action
toast when a keyboard vote auto-advances
- shared useLightbox hook; single keydown subscription reading live state via ref
- album view: per-link thumbs and vote counts; share list shows accept/reject totals
- feedback queries deduped and run concurrently; verdict counts in one scan
- sha256+crc32 hashed during upload streaming; unique index per album
- duplicate content returns the existing photo (race-safe via 23505)
- client hashes locally (WebCrypto) and skips the transfer entirely for
content the album already has
- zip downloads stream S3->response directly using the stored crc32;
pre-hash photos spool once and self-heal (crc via zip, sha via reprocess)
- upload UI: overall progress bar, bytes, live speed, ETA
Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue
(SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived
keys and a fully private bucket, OIDC photographer login with per-request
allowlist checks, client share links with argon2 passwords and lockout,
cookie-based image authorization with sliding expiry, hand-rolled
spec-compliant streaming ZIP downloads with exact Content-Length,
React + Vite gallery frontend, single Docker image, Helm chart for
external S3 + Postgres, and Gitea CI.
Co-Authored-By: Claude <noreply@anthropic.com>