Skip to content

Add emoji reactions on comments and replies (#28)#63

Merged
paulwellnerbou merged 6 commits into
mainfrom
claude/affectionate-mahavira-2f8a2c
May 8, 2026
Merged

Add emoji reactions on comments and replies (#28)#63
paulwellnerbou merged 6 commits into
mainfrom
claude/affectionate-mahavira-2f8a2c

Conversation

@paulwellnerbou
Copy link
Copy Markdown
Owner

Closes #28.

Summary

Slack-style emoji reactions on comments and replies, intentionally simpler than Slack: no recently-used persistence, no custom emojis. Three surfaces:

  • Reactions on comments — every comment node (opener + reply) shows a reaction row below the body and a picker icon next to the existing edit/delete actions.
  • Quick-pick row in the picker popover (👍 ❤️ 😂 🎉 🚀 🤩 🤔 👀) above the full searchable picker for one-click common reactions.
  • :shortcode autocomplete in the composer — type :thumb, get a dropdown like @mention. Tab/Enter inserts the emoji.

Server changes

  • New comment_reactions table keyed on (comment_id, author_client_id, emoji) — a user can react with several different emojis but never twice with the same one.
  • POST /api/documents/:uid/threads/:tid/comments/:cid/reactions body { emoji } toggles atomically (transaction-wrapped to survive double-click races).
  • Wire format: each Comment now carries reactions: [{emoji, count, reacted, authors}] and capabilities.react. The reacted flag is viewer-aware so the UI can highlight the user's own reactions.
  • Validation: 1-32 chars, no whitespace, gated on canComment(role) (readers get 403).
  • Realtime: reuses existing comment.updated event so connected clients debounce-refresh — no new event-type handler needed.

Client changes

  • EmojiReactionPicker uses Radix Themes Popover with frimousse for the full emoji set (lazy-loaded from CDN, ~17KB bundle impact).
  • Reaction chips below each comment; self-reactions get the accent color, hover tooltip lists reactors.
  • emojiShortcodes.ts carries a curated ~120-entry shortcode map (:thumbsup:, :tada:, :+1:, :rocket:, :star_struck:, ...). getActiveShortcode only triggers on word-boundary : so URLs, 1:1, and 12:30 don't pop the menu.
  • InlineComposer keyboard handling: ↑/↓ to navigate, Enter/Tab to insert, Esc to dismiss. Mention takes precedence when both are technically active.

Tests

  • 7 new server tests covering toggle, multi-user/multi-emoji, replies, invalid input, readers, cross-thread targets, capabilities.
  • 8 new client tests covering shortcode detection (URLs / ratios / times rejected), case-insensitive query, exact/prefix/substring ranking, +1/-1 aliases, and limit enforcement.
  • All 47 existing comment tests + all existing client tests pass.

Reviewer notes

  • The reactor's display_name is snapshotted at react time. A subsequent rename will leave old reactions showing the old name in tooltips — acceptable drift, called out in the schema comment.
  • frimousse fetches its emoji dataset from cdn.jsdelivr.net lazily on first picker open. To avoid the third-party fetch, pass emojibaseUrl="..." to EmojiPicker.Root (noted in the file's top comment).
  • The shortcode map is intentionally not exhaustive — users wanting an obscure emoji fall back to the full picker. Easy to extend at the top of emojiShortcodes.ts.

Test plan

  • Open a comment thread, click the smiley icon, verify the quick row + full picker render
  • React with 👍 from one user, confirm a second user sees the chip with count: 1 and reacted: false
  • Click the chip again from the original user — the chip disappears (toggle off)
  • In the composer, type :thum — autocomplete menu appears with 👍 / 👎 / etc.
  • Tab to insert, verify :thum is replaced with the emoji
  • Type https://example.com:443 — no autocomplete fires
  • As a reader-role visitor, confirm the reaction picker doesn't appear and the API returns 403

🤖 Generated with Claude Code

Implements Slack-style reactions, intentionally simpler than Slack: no
recently-used persistence, no custom emojis. Reactions live on every
comment node (opener and replies), with a curated quick-pick row plus
the full emojibase set via frimousse, and a `:shortcode` autocomplete
in the composer for keyboard-first users.

Server:
- New `comment_reactions` table keyed on (comment_id, author_client_id,
  emoji) so a user can react with multiple distinct emojis but never
  duplicate the same one.
- New `POST /:uid/threads/:tid/comments/:cid/reactions` toggles the
  viewer's reaction atomically, returns the updated thread, broadcasts
  `comment.updated` so existing realtime subscribers refresh.
- Each Comment in the wire format now carries `reactions` and a `react`
  capability; viewer-aware `reacted` flag lights up the user's own
  reactions.
- Validation: 1-32 chars, no whitespace, gated on `canComment` (readers
  get 403).

Client:
- EmojiReactionPicker uses Radix Themes Popover with a quick-pick row
  (👍 ❤️ 😂 🎉 🚀 🤩 🤔 👀) above the full frimousse-powered picker.
- `:shortcode` autocomplete in InlineComposer mirrors the @mention UX:
  curated map of ~120 GitHub-style shortcodes, word-boundary trigger
  (URLs / 1:1 / 12:30 don't fire), keyboard nav with ↑↓ Enter/Tab Esc.
- Reaction chips render below each comment body; self-reactions get the
  accent color, hover tooltip shows reactor names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Slack-style emoji reactions to inline comments/replies, including a reaction picker UI, :shortcode composer autocomplete, and server persistence + API/wire support.

Changes:

  • Server: introduce comment_reactions storage and a toggle reaction endpoint; include reactions + capabilities.react in thread wire responses.
  • Client: render reaction chips + picker on each comment node; add :shortcode emoji autocomplete in the inline composer.
  • Tests: add server reaction tests and client shortcode/autocomplete tests.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
bun.lock Locks new dependency for emoji picker (frimousse).
apps/web/package.json Adds frimousse dependency for the picker UI.
apps/server/src/db.ts Adds comment_reactions table + indexes and TypeScript row type.
apps/server/src/routes/threads.ts Adds POST .../reactions toggle endpoint and threads wire support for reactions/capabilities.
apps/server/test/comments.test.ts Adds server tests for reactions toggle, validation, permissions, and scoping.
apps/web/src/lib/api.ts Extends thread/comment types with reactions + node react capability; adds client toggle API wrapper.
apps/web/src/components/DocumentLayout.tsx Wires reaction toggling into the discussion UI flow and refreshes threads after toggles.
apps/web/src/components/inline-comments/InlineCommentsLayer.tsx Threads onReact handler down into inline comments UI.
apps/web/src/components/inline-comments/InlineCommentsList.tsx Threads onReact handler to the list/card level.
apps/web/src/components/inline-comments/InlineThreadCard.tsx Passes onReact into each comment/reply row and preserves react capability during proposal transforms.
apps/web/src/components/inline-comments/InlineCommentRow.tsx Renders reaction chips, adds picker trigger, and implements per-row toggle handling.
apps/web/src/components/inline-comments/EmojiReactionPicker.tsx New popover-based emoji picker with quick-pick row + searchable full picker.
apps/web/src/components/inline-comments/emojiShortcodes.ts New curated shortcode map + detection/filtering helpers for composer autocomplete.
apps/web/src/components/inline-comments/InlineComposer.tsx Adds :shortcode autocomplete UI and keyboard handling (with mention precedence).
apps/web/src/styles/app.css Adds styling for reaction chips, picker popover, and shortcode menu icons.
apps/web/test/discussion-ui.test.ts Adds client tests for shortcode detection/filtering/ranking/limits.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/src/components/inline-comments/EmojiReactionPicker.tsx Outdated
Comment thread apps/web/src/components/inline-comments/emojiShortcodes.ts Outdated
Comment thread apps/web/src/components/inline-comments/InlineCommentRow.tsx Outdated
Comment thread apps/server/src/routes/threads.ts Outdated
- EmojiReactionPicker: drop the nested <button>. Themes' Popover.Trigger
  renders its own button and strips `asChild`; pass styling + a11y
  props to the Trigger directly with the icon as its child.
- filterShortcodes: scan the full map before slicing so `:s` returns
  `:star` even when earlier prefix matches fill the bucket; promote
  exact matches to the top deterministically.
- Reaction chip a11y: stop hiding the emoji from AT and add an
  aria-label that includes emoji + count + whether the viewer
  reacted, so screen readers don't announce a bare "2".
- toggleCommentReaction: scope the existence check + delete by
  `doc_uid` for defense-in-depth + consistency with the insert.
- Add regression tests for the shortcode scan + exact-match promotion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Comment thread apps/server/src/routes/threads.ts Outdated
Comment thread apps/server/src/routes/threads.ts Outdated
- Reaction toggle: replace SELECT-then-INSERT/DELETE inside a deferred
  transaction with `INSERT OR IGNORE` + conditional DELETE keyed off
  `result.changes`. Single atomic statement per write — no transaction,
  no PK-violation 500 path under concurrent same-user same-emoji races.
- listThreads: batch-load reactions for all comment ids on the page
  in one query, pass the preloaded map into toThreadWire. Single-thread
  mutation handlers keep the per-thread fallback (one extra query each
  is fine; the listing endpoint was the N+1 hotspot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.

Comment thread apps/web/src/components/inline-comments/emojiShortcodes.ts Outdated
Comment thread apps/server/src/routes/threads.ts Outdated
Comment thread apps/server/src/routes/threads.ts Outdated
Comment thread apps/server/src/db.ts Outdated
Comment thread apps/web/src/lib/api.ts
- emojiShortcodes.getActiveShortcode: align word-boundary check with
  @mention detection — reject only when prev is alphanumeric/underscore
  rather than rejecting any non-whitespace. URLs/times/ratios still
  short-circuit (digit/letter precedes the colon), but punctuation
  and brackets like `(:thumb` and `":thumb` now trigger correctly.
- loadCommentReactionsWire: batch the IN-clause into chunks of 500 so
  documents with thousands of comment ids never exceed SQLite's
  SQLITE_MAX_VARIABLE_NUMBER (999 on older builds).
- toggleCommentReaction: wrap INSERT OR IGNORE + conditional DELETE in
  a transaction. Each statement is individually atomic, but the pair
  isn't — a third party deleting between our two writes would silently
  flip the user's toggle. Cheap insurance against future multi-process
  bun:sqlite runtimes.
- comment_reactions index: replace two single-column indexes with one
  composite (doc_uid, comment_id, created_at) so the hot read covers
  filter + sort without a filesort.
- toggleCommentReaction (client): return the updated Thread; caller
  splices it into local state instead of doing a full listThreads
  refetch on every reaction toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Comment thread apps/server/src/db.ts
Comment thread apps/web/src/components/inline-comments/EmojiReactionPicker.tsx Outdated
- comment_reactions PK now leads with doc_uid so the schema's
  uniqueness scope matches every read+write (all of them filter by
  doc_uid). Without doc_uid in the PK, a hypothetical comment-id
  collision across documents would clash on rows that belong to
  unrelated docs. Added migrateCommentReactionsPrimaryKey to rebuild
  any pre-fix table created during this PR's development; idempotent
  via a `pk position` check.
- EmojiReactionPicker quick-row: add `role="group"` so the existing
  aria-label is exposed to assistive tech and the buttons read as a
  grouped set.
- New db migration test exercises the rebuild path on a seeded
  pre-fix table and confirms idempotence on a second open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.

Comment thread apps/server/src/routes/threads.ts Outdated
Comment thread apps/server/src/db.ts Outdated
Comment thread apps/web/src/styles/app.css
- loadCommentReactionsWire: ORDER BY (comment_id, created_at) instead
  of just created_at so the sort lines up with the
  (doc_uid, comment_id, created_at) index — the planner can now
  satisfy filter + sort straight from the index instead of a temp
  filesort. Downstream grouping is per-comment-id, so the cross-
  comment order doesn't matter; only oldest-first within each comment
  does, which the new ORDER BY preserves.
- db.ts index comment: rewrite to match the actual planner behavior
  (was misleading about why the index helps).
- .ic-icon-btn:hover: scope to :not(:disabled) so disabled icon
  buttons (e.g. an in-flight reaction toggle) don't visually respond
  to hover while their dimmed/cursor-default styling says otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

@paulwellnerbou paulwellnerbou merged commit 9861fe0 into main May 8, 2026
5 checks passed
@paulwellnerbou paulwellnerbou deleted the claude/affectionate-mahavira-2f8a2c branch May 8, 2026 05:15
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.

Enable emoji reactions on comments and replies

2 participants