Skip to content

fix(timeline): a post could be liked and disliked at once, and the Cat's replies were unlabelled - #817

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/reaction-exclusivity
Aug 28, 2026
Merged

fix(timeline): a post could be liked and disliked at once, and the Cat's replies were unlabelled#817
github-actions[bot] merged 1 commit into
mainfrom
fix/reaction-exclusivity

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Two things found sweeping the timeline in production.

1. Liking and disliking were not actually exclusive

Like a post, then dislike it: both buttons render as active, and the like count keeps a number with no row behind it.

The RPCs do enforce exclusivity — liking DELETEs any dislike row, and the reverse. What neither did was recompute the count belonging to the reaction it had just removed:

timeline_likes         no row for (event, user)   ← correctly deleted
timeline_event_stats   like_count = 1             ← never recomputed
                       dislike_count = 1

That number is wrong for every reader, not just the person who clicked, and it stays wrong — nothing recomputes these except the next reaction on the same post.

All four functions now recount both totals whenever either changes and return both. Toggle-off cannot move the opposite count but returns it anyway, so all four responses have one shape. Existing skewed rows are recounted from the membership tables, which were right all along.

DROP + CREATE was forced by the changed return type, so grants are restored explicitly — a reaction that silently stopped being callable by authenticated would look exactly like the bug being fixed.

The client half was the same mistake from the other side: handleLike only wrote userLiked/likesCount, leaving the opposite button lit. It now clears the opposite optimistically and takes both totals from the response instead of inferring them; a failed switch restores both sides.

Rehearsed against production in a transaction and rolled back: like → (1,0), dislike → (0,1), rows and cached stats agree, one already-skewed row repaired.

2. A Cat reply looked like a person's

is_cat_reply: true is stamped on every Cat reply under a comment saying it exists "so the UI can render a Cat reply distinctly rather than leaving a reader to work out from the avatar that this one was written by an agent". Nothing read it — two writers, zero readers.

That is the actual gap versus how @grok works: the threading was already right (the Cat replies as a child of the post that tagged it), but the model's replies were not visibly the model's. Attributing machine-written text to a human reader is not a styling detail.

Cat-authored posts now carry the same AI badge the mention menu already uses — one definition of what the Cat looks like. Author is the primary signal; the metadata flag is honoured too. @catalogue is not badged.

Both proven by mutation.

🤖 Generated with Claude Code

https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5

…nt behind neither

Found sweeping the timeline in production. Like a post, then dislike it:
both buttons render as active, and the like count keeps a number that
has no row behind it.

The two reactions are meant to be exclusive and the RPCs enforce it —
liking DELETEs any dislike row, and the reverse. What neither did was
recompute the count belonging to the reaction it had just removed. So:

  timeline_likes         no row for (event, user)   ← correctly deleted
  timeline_event_stats   like_count = 1             ← never recomputed
                         dislike_count = 1

That number is wrong for every reader, not just the person who clicked,
and it stays wrong: nothing recomputes these except the next reaction on
the same post. The UI reads timeline_event_stats, so the post shows a
like nobody has given.

All four functions now recount BOTH totals whenever either changes, and
return both. Toggle-off cannot move the opposite count, but returns it
anyway so all four responses have one shape — a caller that has to
remember which of four carries which field will eventually get it wrong.
The existing skewed rows are recounted from the membership tables, which
were right the whole time; only the cache drifted.

DROP + CREATE was forced by the changed return type, so the grants are
restored explicitly rather than left to the default: a reaction that
silently stopped being callable by `authenticated` would look exactly
like the bug being fixed.

The client half was the same mistake from the other side. handleLike
only ever wrote userLiked/likesCount, so it left the opposite button lit
and its count untouched. It now clears the opposite optimistically and
then takes both totals from the response instead of inferring them. A
failed switch restores both sides rather than leaving the post with
neither.

Rehearsed against production inside a transaction and rolled back:
like → (1,0), dislike → (0,1), rows and cached stats agree, and the
repair statement corrected one already-skewed row.

Proven by mutation: dropping the opposite-clearing fails the
switch-sides test and only that one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
@github-actions
github-actions Bot merged commit acb1bca into main Aug 28, 2026
8 checks passed
@github-actions
github-actions Bot deleted the fix/reaction-exclusivity branch August 28, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant