The picker's delete "x" overlaps the glyph in a tightly packed grid,
which is too easy to hit by accident on a touch screen. Adding and
deleting custom emoji now live in their own modal under the account
menu, with delete gated behind the same confirm() every other
destructive action in the app uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Site-wide, any user can upload -- usable both as reactions and inline
in message text via :shortcode:, alongside the existing built-in
Unicode picker. A :shortcode: reference is stored/sent as literal
text (same as the built-in shortcode convention) and resolved to an
image at render time, so it degrades to plain text if the emoji is
later deleted.
Backend: new custom_emoji table (shortcode unique, sized to fit
MessageReaction.emoji's existing column alongside its colons), upload/
list/delete endpoints (delete restricted to uploader or site admin).
Frontend: a CustomEmojiProvider context feeds a new "Custom" category
in the emoji picker (inline upload + hover-to-remove), extends the
composer's shortcode autocomplete, and a shared EmojiGlyph resolver
renders custom emoji wherever a value can appear -- message text,
reaction pills, and the picker itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Shows up to 8 most-recently-picked emoji, pinned above the regular
categories, whenever the picker isn't in search mode -- shared by both
the composer's insert-emoji button and message reactions, since both
go through the same EmojiPicker component. Stored in localStorage
(frontend/src/lib/recentEmoji.ts), per-browser rather than synced
across devices, matching this app's existing local-only preferences
(e.g. the resizable-panel widths).
Verified in-browser: no section when empty, a pick is recorded and
shows up on reopen, order is most-recent-first, re-picking an already-
recent emoji moves it to the front without duplicating, and the list
caps at 8 by dropping the oldest entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reacting to a message near the bottom of the scrolled list opened a
picker that ran off-screen and couldn't be used -- placement was
hardcoded to "below" regardless of the trigger's actual position.
Now computed per-click from the trigger's bounding rect against
available viewport space, matching the composer's own picker (which
was already positioned dynamically, just always "above" since it's
pinned to the bottom of the screen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A search box in EmojiPicker filters the flat emoji list by name/keyword
instead of only browsing by category, shared by both the composer's
emoji trigger and the message reaction picker.
Names/keywords didn't exist anywhere in this codebase before (emoji.ts is
just raw unicode characters). Rather than hand-typing entries for all 928
unique emoji -- a lot of manual work, and unlike the codepoints themselves
a wrong name only means a bad search result, not a broken emoji, so the
accuracy argument for hand-typing didn't apply here -- generated
emojiNames.ts once from emojibase-data (MIT licensed), matched against
emoji.ts by codepoint (normalizing variation-selector differences between
the two sources). emojibase-data itself was never added to package.json;
it was only ever a one-time generation tool, so this ships with zero new
runtime dependency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.