From a428fe6957bda6f52dc1f6baed4912cc7f7b2d41 Mon Sep 17 00:00:00 2001 From: fabi Date: Wed, 12 Aug 2026 09:15:07 +0200 Subject: [PATCH] fix(social): make the counts clients patch with ban-aware, like the view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 028 added `NOT is_banned` to v_feed.like_count and v_feed.comment_count, but not to the two scalar counts in social.rs — which are returned in the response AND broadcast over SSE, and which clients use to patch a card in place rather than refetching. So the two disagreed the moment anyone was banned: the host bans a guest, the feed correctly drops to the lower number, and the very next like on that photo pushes the unfiltered count back to every open client — including the host's, who is watching that number to confirm the ban took. It stayed wrong until a full page-1 refetch. Both call sites carried comments asserting they mirror the view. 028 made those comments false without touching them; this makes them true again. --- backend/src/handlers/social.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/src/handlers/social.rs b/backend/src/handlers/social.rs index 26aac23..6bc1bc7 100644 --- a/backend/src/handlers/social.rs +++ b/backend/src/handlers/social.rs @@ -104,8 +104,17 @@ pub async fn toggle_like( // itself is already committed, so a failed count must not fail the request — but we // also must NOT broadcast/return a bogus 0 (that would push like_count: 0 to every // client until the next event). On error we skip the broadcast and return null. + // The `NOT u.is_banned` join is what makes "mirrors v_feed.like_count" true. Migration 028 + // added it to the view and not here, so the two disagreed the moment anyone was banned: the + // host bans a guest, the feed correctly drops to the lower number, and then the very next like + // on that photo broadcasts the UNFILTERED count back to every open client — including the + // host's, who is watching that number to confirm the ban took. It stayed wrong until a full + // page-1 refetch. `like.user_id` is NOT NULL REFERENCES "user"(id), so the inner join can + // neither drop nor duplicate a row. let like_count = sqlx::query_scalar::<_, i64>( - "SELECT COUNT(DISTINCT user_id) FROM \"like\" WHERE upload_id = $1", + "SELECT COUNT(DISTINCT l.user_id) FROM \"like\" l \ + JOIN \"user\" u ON u.id = l.user_id \ + WHERE l.upload_id = $1 AND NOT u.is_banned", ) .bind(upload_id) .fetch_one(&state.pool) @@ -222,8 +231,13 @@ pub async fn add_comment( // over the same deleted_at filter is identical since comment.id is the PK). The // count + broadcast are a UI optimisation — the comment is already committed, so a // failure here must not fail the request. Swallow the error and skip the broadcast. + // `NOT u.is_banned` for the same reason as `like_count` above — see that comment. Migration + // 028 put this filter in `v_feed.comment_count` and `Comment::list_for_upload`, but not here, + // so posting a comment pushed the pre-ban total back to every client. if let Ok(comment_count) = sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM comment WHERE upload_id = $1 AND deleted_at IS NULL", + "SELECT COUNT(*) FROM comment c \ + JOIN \"user\" u ON u.id = c.user_id \ + WHERE c.upload_id = $1 AND c.deleted_at IS NULL AND NOT u.is_banned", ) .bind(upload_id) .fetch_one(&state.pool)