fix(timeline): a post could be liked and disliked at once, and the Cat's replies were unlabelled - #817
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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+CREATEwas forced by the changed return type, so grants are restored explicitly — a reaction that silently stopped being callable byauthenticatedwould look exactly like the bug being fixed.The client half was the same mistake from the other side:
handleLikeonly wroteuserLiked/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: trueis 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
@grokworks: 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
AIbadge the mention menu already uses — one definition of what the Cat looks like. Author is the primary signal; the metadata flag is honoured too.@catalogueis not badged.Both proven by mutation.
🤖 Generated with Claude Code
https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5