Gallery & Collections
Gallery & Collections
Section titled “Gallery & Collections”Purpose
Section titled “Purpose”Document the redesigned gallery surface (Gallery v2) and the user-owned
collections (albums) that back it. The home page / always renders
GalleryV2Shell; there is no legacy Gallery route or rollout flag branch. For the full-stack
engineering view see Gallery v2 — Architecture.
Participants
Section titled “Participants”- Brisket renders the gallery shell, toolbar, grid, detail, search, filters,
albums, and smart groups (
features/gallery-v2/). Reads media viaapi.media.listAllMediaand collections viaapi.collections.*. - Sirloin serves
ListAllMedia(cross-character feed, optionally scoped to a collection or a single character) and the collection CRUD / membership RPCs, and owns themedia.collections+media.collection_mediatables.
Cross-character media (ListAllMedia)
Section titled “Cross-character media (ListAllMedia)”ListAllMedia reuses ListMedia’s filtering, carousel loading, example
controls, and presigned-URL logic:
- Empty
character_id⇒ no character scope (the whole user’s gallery). - Set
character_id⇒ the gallery’s “Characters” filter. - Set
collection_id⇒ results restricted to that collection’s members. - Each row carries optional character context (
media_character_*) so the cross-character grid can label and filter without extra lookups. - Type and character filters pass server-side to
ListAllMediafor both the main feed and album views (viaresolveMediaTypehelper).
Paging uses skip_total with a limit+1 sentinel to detect further pages
without a COUNT(*).
Collections (albums)
Section titled “Collections (albums)”- A collection is user-owned (
media.collections, all queries filteruser_id); membership rows live inmedia.collection_mediawith anadded_attimestamp. media_countandcover_pathare computed as subqueries that exclude soft-deleted media;ListCollectionMediaIDslikewise joinsmediaand filtersdeleted_at IS NULL, so the member list stays consistent with the shown count.- An empty (but valid) collection returns no media — callers must not fall back to listing all of the user’s media.
- The cover preview is the most-recently-added non-deleted member.
- Album ordering on the hub is local-only (persisted in
localStorage): the backend has no album ordering. - Filtering inside albums: type and character filters apply server-side for collection-backed and scope-backed albums, client-side for pre-loaded albums (18+). Filters reset when navigating back to the albums hub.
Multi-select & bulk actions
Section titled “Multi-select & bulk actions”- Selection snapshots full
GalleryItemdata intoselectedItemsMapso items survive filter changes that remove them from the current query. - Context-aware actions: archived view shows Download + Unarchive; elsewhere shows Add to album + Download + Archive.
- Count badge on the select button reflects current selection across filters.
- Downloads fire both the
markMediaDownloadtRPC mutation and the PostHogGeneration Downloadedevent for every item.
Download strategy
Section titled “Download strategy”Platform-specific download handling via shareOrDownloadImages:
- Embedded browsers (FB/Instagram on Android): toast, no download.
- Single image on mobile (non-Chrome-iOS):
navigator.share(save to photos). - iOS Safari bulk images: redirect to
/downloadpage (tap-and-hold). - iOS Safari videos: blob auto-download.
- Chrome iOS:
application/octet-streamblob trick. - Desktop / Android: staggered
<a download>clicks.
The detail view pre-warms files from cache via useShareableDownload to stay
within iOS’s transient user activation window.
WebKit browsers often cancel the markMediaDownload request while the file
saves. Brisket therefore calls Sirloin’s SetMediaDownloaded without the
request’s abort signal, so the mark still lands after the browser cancels.
Invariants
Section titled “Invariants”- Ownership is enforced server-side on every collection read/mutation and on add-to-collection (media not owned by the user is rejected).
- Draft characters are excluded from the gallery’s search and character filter.
- Deleting a collection soft-deletes the collection and hard-deletes its membership rows in one transaction.
- PostHog
Generation Downloadedfires for every download (single + bulk).
Error paths
Section titled “Error paths”- Invalid collection / not owned ⇒
collection not found. - Adding media not owned by the user ⇒ rejected before any membership write.
- Cover presign failures degrade to “no cover” (the collection still lists).
- Embedded browser download ⇒ toast “open in a regular browser”.
Known follow-ups
Section titled “Known follow-ups”- Collection media is returned in
created_atorder, while the cover is the most-recently-added member, so the cover and first grid tile can differ. - 18+ gating is session-local and must bind to the real entitlement / age-verification before ship (requires sign-off).
- Per-slide carousel editing needs image-pipeline support; today “Edit” on a carousel edits the cover and yields a standalone photo.
Verification
Section titled “Verification”/on a PR preview: grid, detail, search, filters, albums CRUD, add-to-album, smart groups, favorites/archive.- Multi-select download across carousels + videos + photos on desktop and mobile.
- Create an album, add/remove media, filter inside it, delete media in it, and confirm the count and cover stay consistent.