fix(feed): filter on the server, exactly, and keep banned uploads out of the chips
Filtering was split across two independent client-side states and applied to whatever page 1 happened to hold, by caption SUBSTRING. So a tag chip selected in the list view was silently still applied in the grid without being shown; a filter matched photos whose caption merely contained the text; and anything past the first page was invisible to it. Verified against the seeded data: `hashtag=tanz` returned 6 photos by substring, 1 by tag. `FeedQuery` now carries `hashtag` (single, list view), `hashtags` (CSV, OR'd, grid chips) and `uploader` (exact, AND'd), normalised through one function that trims, strips `#`, lowercases and dedupes, and yields None when empty — so an empty filter means "no filter", never "match nothing". The two SQL branches collapse into one with `h.tag = ANY($4)`. Tag-OR plus tag+user-AND is a specified feature, not an accident: `e2e/specs/03-feed/filter-search.spec.ts` and USER_JOURNEYS §8 pin it, which is why the semantics moved to the server rather than being simplified away. Tags travel as CSV safely because the backend restricts them to ASCII alphanumerics and `_`; `uploader` stays a single exact parameter because a display name can contain a comma. New `GET /api/v1/uploaders` reads `v_feed`, so banned and hidden uploaders are excluded for free. Migration 021 gives `v_hashtag_counts` the same treatment. It counted every upload regardless of the uploader's ban state, so banning a guest left their tags in the chip list as ghost filters that lead to an empty feed. Verified: after banning the guest who owned all six `tanz*` photos, the chips went 6 -> 0. `?limit=-5` returned a 500 — only the upper bound was clamped, so Postgres was asked for `LIMIT -4`. Clamped at both ends. `is_banned` is added to `/me/context` so the client can show a read-only notice instead of letting a banned guest discover the ban one 403 toast at a time. `add_comment` sorts and dedupes hashtags on the normalised key, matching the upload path — the two disagreed, which is a lock-ordering deadlock between concurrent upserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,14 @@ pub struct MeContextDto {
|
||||
/// The gallery has been released and the export snapshotted — uploads are permanently
|
||||
/// closed for this run (release ⇒ lock, and reopening regenerates).
|
||||
pub gallery_released: bool,
|
||||
/// This guest is banned: a deliberately READ-ONLY ban (see `handlers/host.rs`) — they keep
|
||||
/// the feed and the keepsake, but every write is refused.
|
||||
///
|
||||
/// Exposed so the UI can SAY so. Without it the client had no idea, so the upload button,
|
||||
/// the like button and "Löschen" all rendered enabled and returned 403 "Du bist gesperrt."
|
||||
/// on every tap — a guest tapping upload repeatedly with nobody to ask. The lock case
|
||||
/// (`uploads_locked`) has always been surfaced for exactly this reason; a ban was not.
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
pub async fn get_context(
|
||||
@@ -110,5 +118,6 @@ pub async fn get_context(
|
||||
storage_quota_enabled,
|
||||
uploads_locked,
|
||||
gallery_released,
|
||||
is_banned: user.is_banned,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user