# Custom Emoji > **Area:** custom-emoji > **Status:** Active > **Last updated:** 2026-07-05 ## Overview Custom emoji lets users upload small image emoji, use them in chat messages and reactions, and sync the image bytes to connected peers over the WebRTC data channel (and to sibling devices via `account_sync`). The signaling server never stores emoji assets. Internal UI and NgRx wiring: [`toju-app/src/app/domains/custom-emoji/README.md`](../../toju-app/src/app/domains/custom-emoji/README.md). Chat composer integration: [`toju-app/src/app/domains/chat/README.md`](../../toju-app/src/app/domains/chat/README.md). ## Responsibilities - Validate uploads (size, MIME), persist image assets locally, and track per-user **saved library** membership. - Rank shortcuts by local usage (not synced across devices). - Sync assets P2P (`custom-emoji-*` envelopes) and proactively push referenced emoji when sending messages. - Relay the same envelopes on `account_sync` for multi-device library convergence. - Expose `CustomEmojiPickerComponent` for composer and reactions. This area does **not** own: - Message send/edit transport → [messaging.md](messaging.md). - Profile avatar bytes → `toju-app/src/app/domains/profile-avatar/README.md`. - Server-side storage (none). ## Key concepts - **Custom emoji asset** — image with `id`, `name`, `mime`, `size`, `hash`, `creatorUserId`, `dataUrl` (or reconstructed from chunks). - **Known emoji** — synced for rendering; not necessarily in the picker. - **Saved emoji** — in the active user's library (`metoyou_custom_emoji_saved:`); shown in picker and shortcut row. - **Token** — stable wire form `:emoji[id](name)` in message/reaction bodies. - **Composer alias** — draft form `:name:` rewritten to a token on send when the name is known. - **Shortcut row** — seven most-used saved entries plus opener for full picker. --- ## Peer envelope contract (P2P) | type | Payload | |------|---------| | `custom-emoji-summary` | `{ customEmojiSummaries: [{ id, hash, updatedAt }] }` | | `custom-emoji-request` | `{ ids: string[] }` | | `custom-emoji-full` | manifest (`customEmojiTransfer`) ± inline bytes | | `custom-emoji-chunk` | `{ customEmojiId, index, total, data }` base64 | **Handshake:** on peer connect both sides send summaries; receiver requests stale/missing ids; owner sends manifest then chunked payloads via buffered sends. **Proactive push:** outgoing chat/DM messages scan for tokens and push assets to connected peers in parallel with the message event. **Inline threshold:** assets ≤ `CUSTOM_EMOJI_INLINE_MAX_JSON_BYTES` (48 KiB) ship in one `custom-emoji-full`; larger assets use manifest + chunks. **Repair path:** incoming messages and `chat-sync-batch` scan for tokens and request missing assets from the sender. ### Multi-device (`account_sync`) Relayable types (`account-sync.rules.ts`): `custom-emoji-summary`, `custom-emoji-request`, `custom-emoji-full`, `custom-emoji-chunk`. See [signaling.md](signaling.md) and [authentication.md](authentication.md). --- ## Business rules and invariants - Max upload **1 MB**; MIME: WebP, GIF, JPEG/JPG (same set as profile avatars). - Library membership is **per user id**, not per device — second account on same machine does not inherit another user's saved set. - Seeing an emoji in chat does **not** add it to the library; user must **Add to emoji library** from context menu. - Remove from library hides picker entry but keeps asset for messages that already reference it. - Chunks stay below SCTP-safe sizes; oversized single sends can trigger `RTCDataChannel.onerror` even when back-pressure is idle. - Persist only when reconstructed `dataUrl` matches manifest **size and hash**; corrupt rows are dropped before advertising summaries. - Placeholder rendering avoids flashing raw tokens while assets are in flight. --- ## Storage | Runtime | Asset bytes | Library membership | |---------|-------------|-------------------| | Browser | IndexedDB `customEmojis` (per-user DB scope) | `localStorage` `metoyou_custom_emoji_saved:` | | Electron | SQLite `custom_emojis` (shared desktop DB) | same localStorage key | | Capacitor | SQLite `custom_emojis` in `metoyou__` | same localStorage key | API: `DatabaseService.saveCustomEmoji` / `getCustomEmojis` / `deleteCustomEmoji`. --- ## Technical implementation - Rules: `domains/custom-emoji/domain/custom-emoji.rules.ts` - Service: `CustomEmojiService`; effects: `CustomEmojiSyncEffects` - Picker: `feature/custom-emoji-picker/` - Context menu: `data-custom-emoji` / `data-custom-emoji-library` attributes on rendered hosts --- ## Testing - `custom-emoji.rules.spec.ts`, `custom-emoji.service.spec.ts`, `custom-emoji-picker.component.spec.ts` - `account-sync.rules.spec.ts` (relayable types) - E2E: `e2e/tests/chat/custom-emoji-user-binding.spec.ts` --- ## Security considerations - Image-only, size-capped payloads before persist or broadcast. - Assets reach only connected peers (or same-account devices via `account_sync`); server never proxies bytes. --- ## Known limitations - Usage counts and shortcut ranking are **local only**. - Electron asset table is **shared across OS users** on one desktop install; library keys remain per MetoYou user id. --- ## Related features - [messaging.md](messaging.md) — tokens in message bodies, proactive push on send - [signaling.md](signaling.md) — `account_sync` - [mobile-capacitor.md](mobile-capacitor.md) — Capacitor SQLite path ## Changelog | Date | Change | |------|--------| | 2026-07-05 | Restructured to match messaging doc style; fixed duplicate sections; Capacitor + account_sync |