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
+43
View File
@@ -0,0 +1,43 @@
import { useEscapeKey } from '../hooks/useEscapeKey'
import { EMOJI_CATEGORIES } from '../lib/emoji'
import './EmojiPicker.css'
interface EmojiPickerProps {
onPick: (emoji: string) => void
onClose: () => void
placement?: 'above' | 'below'
align?: 'left' | 'right'
}
export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'left' }: EmojiPickerProps) {
useEscapeKey(onClose)
return (
<>
<div className="emoji-picker-scrim" onClick={onClose} />
<div
className={`emoji-picker emoji-picker-${placement} emoji-picker-${align}`}
role="menu"
>
{EMOJI_CATEGORIES.map((category) => (
<div key={category.label} className="emoji-picker-category">
<div className="emoji-picker-category-label">{category.label}</div>
<div className="emoji-picker-grid">
{category.emoji.map((emoji) => (
<button
key={emoji}
type="button"
role="menuitem"
className="emoji-picker-item"
onClick={() => onPick(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
))}
</div>
</>
)
}