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:
13
backend/migrations/021_hashtag_counts_respect_bans.down.sql
Normal file
13
backend/migrations/021_hashtag_counts_respect_bans.down.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Restore the pre-021 definition (no ban/hide filtering) exactly as 004 created it.
|
||||
DROP VIEW IF EXISTS v_hashtag_counts;
|
||||
|
||||
CREATE VIEW v_hashtag_counts AS
|
||||
SELECT
|
||||
h.event_id,
|
||||
h.tag,
|
||||
COUNT(uh.upload_id) AS upload_count
|
||||
FROM hashtag h
|
||||
JOIN upload_hashtag uh ON uh.hashtag_id = h.id
|
||||
JOIN upload u ON u.id = uh.upload_id AND u.deleted_at IS NULL
|
||||
GROUP BY h.event_id, h.id, h.tag
|
||||
ORDER BY upload_count DESC;
|
||||
31
backend/migrations/021_hashtag_counts_respect_bans.up.sql
Normal file
31
backend/migrations/021_hashtag_counts_respect_bans.up.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- v_hashtag_counts: apply the same visibility rules as v_feed.
|
||||
--
|
||||
-- The chip row and the grid's tag picker are both fed by this view, but it has only ever
|
||||
-- filtered `u.deleted_at IS NULL`. Migration 011 added ban/hide filtering to the feed and
|
||||
-- never reached here, so the two disagreed about which uploads exist:
|
||||
--
|
||||
-- * A host bans a guest who posted 3 of the 12 `#tanz` photos. The chip keeps reading
|
||||
-- "#tanz 12"; tapping it returns 9. The count is presented as authoritative and is not.
|
||||
-- * A tag used ONLY by a banned or hidden guest stays in the chip row and in the tag
|
||||
-- picker as a selectable option that leads to an empty feed — a ghost filter that
|
||||
-- cannot be cleared because there is nothing wrong with it to see.
|
||||
--
|
||||
-- Bans are exactly the moment a host is watching these numbers to confirm the moderation
|
||||
-- took effect, so a stale count reads as "the ban didn't work".
|
||||
--
|
||||
-- Same predicate as v_feed (see 016_display_derivative.up.sql), joined through `user`.
|
||||
DROP VIEW IF EXISTS v_hashtag_counts;
|
||||
|
||||
CREATE VIEW v_hashtag_counts AS
|
||||
SELECT
|
||||
h.event_id,
|
||||
h.tag,
|
||||
COUNT(uh.upload_id) AS upload_count
|
||||
FROM hashtag h
|
||||
JOIN upload_hashtag uh ON uh.hashtag_id = h.id
|
||||
JOIN upload u ON u.id = uh.upload_id AND u.deleted_at IS NULL
|
||||
JOIN "user" usr ON usr.id = u.user_id
|
||||
WHERE usr.uploads_hidden = FALSE
|
||||
AND usr.is_banned = FALSE
|
||||
GROUP BY h.event_id, h.id, h.tag
|
||||
ORDER BY upload_count DESC;
|
||||
Reference in New Issue
Block a user