feat(rooms): per-room Files tab in room info (LC-87) #151
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-87-room-files-tab"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes LC-87. Adds a "Files" tab next to Docs and Pinned on
/room/{id}/info. Lists every upload attached to a non-soft-deleted message in the room, newest first, paginated. Filter dropdown narrows to images / video / audio / PDF / other. Each card links to the parent message via/room/{id}#msg-{message_id}.Schema
file_uploads(migration 0012) joined tomessagesonfile_uploads.message_id = messages.id; the JOIN naturally excludes soft-deleted messages and orphan uploads.DB layer
db::uploads::FileKindFilterenum + query-string round trip.list_room_files(pool, room_id, kind, before_id, limit): DESC-by-id with abeforecursor for pagination. Mime predicates composed in Rust to keep one SQL plan per filter.Route
GET /room/{id}/info?tab=filesrenders the page with the initial 40-row page.GET /room/{id}/files?kind=X&before=Y&append=true|falseis the HTMX fragment endpoint.append=false(filter change) replaces#files-grid;append=true(load-more) returns OOB rows targeting#files-grid-rows beforeendplus a fresh load-more button (hx-target="#files-load-more" hx-swap="outerHTML") that replaces the one the user clicked.is_room_accessible, so a user not in a private room cannot list its files (acceptance criterion).View / template
RoomFileRowcarries pre-resolved uploader label + anis_imageflag + a one-characterkind_icon(V/A/P/F) for non-image cards. No new icon assets.room/info.htmlgains the Files tab nav entry, the filter dropdown, the initial 40-row grid, the load-more slot.room/file_card.htmlkeyed off acard_room_idlocal, so the page and the fragment endpoint render identical markup.files_fragment.htmlbranches onappend: replace mode emits a fresh#files-grid-rowswrapper; append mode emits OOB rows + a new load-more wrapper.Acceptance criteria coverage
/room/{id}/fileslists every upload (filename, uploader, date, thumbnail / icon).#msg-{id}).deleted_at IS NULL).is_room_accessibleis the gate).Out of scope (open questions, deferred)
Tests (
routes_room_files.rs, 6/6 passing): empty notice; seeded files appear;kind=imageexcludes a PDF; soft-deleted parent hides the upload; outsider 403s on private room files; pagination advances the cursor correctly across the page-1 / page-2 boundary.just checkclean.just testpasses outside the pre-existingroutes_uploadsconcurrent-binary flake (CLAUDE.md-noted; passes alone).Adds a "Files" tab next to Docs and Pinned on `/room/{id}/info`. Lists every upload attached to a (non-soft-deleted) message in the room, newest first, paginated. Filter dropdown narrows to images / video / audio / pdf / other. Each card links back to the parent message via `/room/{id}#msg-{message_id}`. Schema - No new tables. Reuses `file_uploads` (added in migration 0012) joined to `messages` on `file_uploads.message_id = messages.id` so soft-deleted messages and orphan uploads (message_id IS NULL) are filtered out by the join. No new migration. DB layer - `db::uploads::FileKindFilter` enum + `from_query`/`as_query` round-trip. - `db::uploads::list_room_files(pool, room_id, kind, before_id, limit)` returns uploads in DESC `id` order. The cursor is the smallest id on the current page; the route handler peeks one extra row past `limit` to decide whether to render the load-more button. Route - `GET /room/{id}/info?tab=files` renders the page with the initial 40-row page. - `GET /room/{id}/files?kind=X&before=Y&append=true|false` is the HTMX fragment endpoint. `append=false` (filter change) replaces `#files-grid`; `append=true` (load-more click) returns the new rows as an OOB swap targeting `#files-grid-rows beforeend` plus a fresh load-more button that replaces the one the user clicked. - Both endpoints reuse the existing `is_room_accessible` predicate, so a user not in a private room cannot list its files (acceptance criterion). View / template - `RoomFileRow` carries the pre-resolved uploader label + an `is_image` flag + a one-character `kind_icon` (V/A/P/F) for the non-image card. No new icon assets needed. - `info.html` gains the Files tab nav entry, the filter dropdown, the initial 40-row grid, and the load-more slot. Cards are factored into `room/file_card.html` (a partial keyed off a `card_room_id` local) so the page and the fragment endpoint render identical markup. - `files_fragment.html` branches on `append`: replace mode emits a fresh `#files-grid-rows` wrapper; append mode emits OOB rows + a new load-more wrapper (`hx-target="#files-load-more" hx-swap="outerHTML"`), so the page is HTML-valid and HTMX swaps land in the right slot. Out of scope (open questions, deferred) - Uploader-typeahead and date-range filters (the spec listed them; kind-filter alone covers the most common find-this-file case). - Bulk zip download. - Storage-usage display (waits for LC-93 quotas). Tests (`routes_room_files.rs`, 6/6 passing) - empty room shows the no-files notice; - seeded files appear in the tab; - `?kind=image` excludes a seeded PDF; - a soft-deleted parent message hides the upload; - outsider 403s on `/room/{private}/files`; - pagination: page 1 returns the newest 40, the load-more cursor advances correctly, and page 2 contains the oldest 5. `just check` clean. `just test` passes outside the pre-existing `routes_uploads` concurrent-binary flake (CLAUDE.md-noted; passes alone).