Add emoji picker and message reactions (Gitea issue #11)

Composer gains an emoji picker (static curated unicode list, insert at
cursor position) and messages gain Slack/Mattermost-style reactions:
react with any emoji, toggle off by reacting again, see who reacted via
a tooltip on each pill.

Backend: MessageReaction model (unique on message_id+user_id+emoji backs
toggle semantics), WS "reaction" envelope broadcasts the full recomputed
reaction list per message (same approach as message edits), REST message
list embeds reactions so a reload doesn't lose state that only arrived
over WS.

Frontend: shared EmojiPicker component (anchored popover, Escape/outside-
click dismiss via new useEscapeKey hook) used by both the composer and a
new hover-revealed reaction trigger on each message row.
This commit is contained in:
2026-08-14 15:37:59 -06:00
parent f2a59f798b
commit c6f90d49fc
22 changed files with 854 additions and 34 deletions
+33 -6
View File
@@ -1,4 +1,4 @@
# KeepItTalking backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads)
# KeepItTalking backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads, emoji & reactions)
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL + Redis. Implements auth, room
CRUD (open and private), room roles (owner/admin/member) and invites, a
@@ -6,8 +6,9 @@ WebSocket chat endpoint that fans out across multiple app-server instances
via Redis pub/sub, Web Push notifications for offline room members, a
site-admin portal (user/room/bot management + an audit log), a bot/
extension layer (scoped API tokens, live bot WebSocket access, incoming and
outgoing webhooks, message editing), and image uploads in chat messages. See
`../ARCHITECTURE.md` for the full system design and the phased build plan.
outgoing webhooks, message editing), image uploads in chat messages, and
emoji reactions on messages. See `../ARCHITECTURE.md` for the full system
design and the phased build plan.
This is an **invite-only site**: there is no public registration endpoint.
Accounts are created by an operator on the app server — see step 4 below.
@@ -119,9 +120,10 @@ app/
and on-disk save/read -- see Image uploads below
cli.py `python -m app.cli create-user` / `generate-vapid-keys`
models/ SQLAlchemy models (users, rooms, room_memberships,
messages, message_images, room_invites,
push_subscriptions, admin_audit_log, api_tokens,
webhooks_incoming, event_subscriptions)
messages, message_images, message_reactions,
room_invites, push_subscriptions,
admin_audit_log, api_tokens, webhooks_incoming,
event_subscriptions)
schemas/ Pydantic request/response models
routers/ auth, rooms, invites, push, admin, bots, webhooks, health
services/ business logic called by routers
@@ -349,6 +351,31 @@ cleanup job for this yet. Not a security issue, since serving still goes
through the same room-membership gate as everything else; just an eventual
disk-space housekeeping item.
## Emoji & reactions
An emoji picker in the frontend composer is purely client-side (a static
curated unicode list, no backend involvement). Message **reactions** are
full-stack: `message_reactions` (`app/models/message_reaction.py`) has
`message_id`, `user_id`, `emoji`, and a `UniqueConstraint` on all three
backing toggle semantics — the same user reacting with the same emoji on
the same message twice removes it (Slack/Mattermost convention).
`message_service.toggle_reaction` is a plain select-then-delete-or-insert,
no upsert needed.
WS `"reaction"` envelope (`room_id`, `message_id`, `emoji`) toggles a
reaction; the server broadcasts the message's **full recomputed** reaction
list (`{"type": "reaction_update", "id", "room_id", "reactions": [...]}`),
not an add/remove delta — same approach `message_update` already uses for
edits, keeping client-side state a simple replace rather than a merge.
`GET /{room_id}/messages` embeds each message's `reactions` too
(`message_service.get_reactions_for_messages`, batched, not N+1), so a page
reload doesn't lose reaction state that only ever arrived over WS.
Scope cuts: no outgoing-webhook event type for reactions (`VALID_EVENT_TYPES`
in `webhook_service.py` is unchanged — same restraint as image uploads), no
reaction-count limit or rate limiting, no custom/uploaded emoji (unicode
only, curated client-side list in `frontend/src/lib/emoji.ts`).
## Notes / scope decisions
- Invite-only site registration: no `POST /api/auth/register`. Accounts are