Site-wide Emoji Support — Design¶
Date: 2026-07-08 Status: Approved
Goal¶
Add first-class emoji support to all user-facing text interfaces: an emoji picker on every compose input, Slack-style :shortcode: autocomplete/conversion while typing, and emoji reactions on feed posts, comments, direct messages, and group messages.
Context — what already works (no changes needed)¶
Storage: PostgreSQL is UTF-8; all body columns (
RMHark,RMHarkComment,DirectMessage.content,GroupMessage.content, chat messages, bios) store emoji natively. No charset migration.Validation: all body validators are Zod length caps only (
lib/rmhark-schema.ts,lib/profile-schema.ts, message API routes) — nothing strips or blocks non-ASCII.Rendering:
components/ui/TwemojiProvider.tsx, mounted at the app root (app/routes/__root.tsx), already converts any native emoji to Twemoji SVGs site-wide.Dependencies:
emoji-picker-react(used incomponents/rmhbox/minigames/emoji-cinema/EmojiKeyboard.tsx),@twemoji/api, andtwemoji-parserare already installed.Prior art: realtime chats (rmhstudy/rmhtype/rmhtube) already have a fixed-set emoji reaction feature via
CHAT_REACTION_EMOJISinlib/shared/chat-constants.tsand theChatPanelcomponents.
Part 2 — Shortcode autocomplete (:fire: → 🔥)¶
A shared useEmojiShortcode hook usable on any textarea/input, with two behaviors:
Autocomplete dropdown: typing
:followed by 2+ characters shows a dropdown of matching emoji (name + glyph), visually consistent with the existing @mention autocomplete inMentionTextarea. Enter/Tab/click inserts the unicode emoji, replacing the partial shortcode. Escape dismisses.Instant conversion: when the user finishes typing a complete known shortcode (
:fire:), it is replaced with the emoji (🔥) immediately in the input, caret preserved after the emoji.
What is sent and stored is plain unicode — zero server or renderer changes.
Shortcode data: a compact static name → emoji JSON generated at build/dev time from unicode-emoji-json (or emojibase) and committed to the repo (~50–100KB), lazy-loaded on first : trigger. No new eager runtime dependency.
Wired into:
MentionTextarea(posts, comments, group chats) — alongside the existing@/#triggers.GhostTextArea(DMs).The plain chat inputs in
shared/ChatPanelandrmhtube/ChatPanel.
Part 3 — Emoji reactions on posts, comments, DMs, group messages¶
Likes remain unchanged; reactions are additive.
Trigger: right-click / long-press¶
Desktop: right-click (
onContextMenu+preventDefault) on a post card, comment, or message bubble opens a floating reaction menu positioned at the cursor.Touch: long-press does the same. Android fires
contextmenunatively on long-press; a small touch-timer fallback handles iOS Safari (which does not).Menu contents: the quick row (👍 ❤️ 😂 😮 😢 🔥 — reuse
CHAT_REACTION_EMOJIS) plus a + button that opens the full emoji picker, so any emoji can be a reaction (Discord/Slack model).
Display¶
Reaction chips under the content: emoji + count (e.g. 🔥 3), sorted by count.
Chip is highlighted when the current user has reacted; clicking a chip toggles that reaction.
Hover tooltip lists who reacted.
Same visual language as the existing realtime-chat reaction pills.
Data model (Option A — per-target tables, chosen)¶
Four new Prisma models, following the existing RMHarkLike / RMHarkCommentLike pattern:
RMHarkReactionRMHarkCommentReactionDirectMessageReactionGroupMessageReaction
Each with: id, emoji String @db.VarChar(32) (ZWJ sequences/flags are multi-codepoint), userId FK, target FK with cascade delete, createdAt, and @@unique([userId, <targetId>, emoji]) (a user can add multiple distinct emoji to one target, but each emoji once).
Rejected alternative: a single polymorphic Reaction table (targetType + targetId) — fewer models, but loses FK integrity/cascade deletes and diverges from the codebase’s established per-target pattern.
API & data flow¶
Toggle endpoints per target type, following the existing like-route pattern (auth required; validates emoji is a non-empty string ≤32 chars).
Post/comment/message fetch payloads include grouped reactions:
[{ emoji, count, reactedByMe }](+ reactor names for tooltips, capped).DM/group-chat reaction updates ride the existing message refresh mechanism (TanStack Query invalidation/polling) — no new realtime infrastructure.
Optimistic UI on toggle.
Out of scope (v1)¶
Notifications for reactions (likes keep theirs; possible follow-up).
Grapheme-aware character counting (emoji continue to cost ≥2 toward length limits — explicitly deselected).
Migrating realtime-chat reactions to the new tables (they keep their socket-based
{emoji: userIds}storage).
Build order¶
Three independent, shippable PRs:
Picker —
EmojiPickerButton+ wiring into all compose surfaces.Shortcodes — dataset generation,
useEmojiShortcodehook, wiring into textareas/inputs.Reactions — Prisma models + migration, API routes, right-click reaction menu, chips UI.
Testing¶
Unit: caret insertion helper (middle/end of text, selection replacement); shortcode matcher (partial query extraction, full-code instant conversion, no false positives on
10:30or URLs); reaction grouping logic.API: toggle route tests (add, toggle off, duplicate-emoji uniqueness, auth, cascade on target delete) following existing like-route test patterns.
Note: no DOM/WebGL test env in this repo (node-only Vitest) — component behavior verified via browser playtest; hooks tested at the pure-logic level.