# Custom Emoji, Stickers & GIF > Target phase: 1.3 (emoji picker) / 2 (custom emoji, stickers, GIF) > Depends on: Phase 1.2 (guild system, permissions) --- ## Overview Three related but distinct features: | Feature | Size | Format | Scope | Storage | |---------|------|--------|-------|---------| | **Custom Emoji** | 128×128 max | PNG, GIF, WebP | Per-guild | Server filesystem | | **Stickers** | 512×512 max | PNG, GIF, WebP, Lottie† | Per-guild + per-user | Server filesystem | | **GIF Picker** | External | GIF (Tenor/Giphy API) | Per-server config | Proxy via server | † Lottie — future, not Phase 2. ### Difference from unicode reactions Unicode emoji reactions (`message_reactions` table, Phase 1.3) use single-codepoint emoji (👍, 🔥, 😂). Custom emoji extend this with server-hosted images referenced as `:emoji_name:` in messages. --- ## Storage & Schema ### Filesystem layout ``` data/ ├── guilds/ │ └── {guild_id}/ │ ├── emojis/ │ │ ├── kappa.png │ │ ├── pogchamp.gif │ │ └── ... │ └── stickers/ │ ├── wave.png │ └── ... ├── users/ │ └── {user_id}/ │ └── stickers/ # personal stickers │ └── ... └── emoji_cache/ # client-side cache (per client, not server) ``` ### Database tables #### guild_emojis ```sql CREATE TABLE guild_emojis ( id TEXT PRIMARY KEY, -- UUID guild_id TEXT NOT NULL, -- FK guilds name TEXT NOT NULL, -- :name: reference (lowercase, alphanumeric + underscore) filename TEXT NOT NULL, -- on-disk filename (id.png) content_type TEXT NOT NULL, -- image/png, image/gif, image/webp file_size INTEGER NOT NULL, -- bytes width INTEGER NOT NULL, -- px height INTEGER NOT NULL, -- px is_animated BOOLEAN NOT NULL, -- true for GIF uploaded_by TEXT NOT NULL, -- FK users created_at TIMESTAMP NOT NULL, UNIQUE(guild_id, name) ); CREATE INDEX idx_guild_emojis_guild ON guild_emojis(guild_id); ``` #### guild_stickers ```sql CREATE TABLE guild_stickers ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, filename TEXT NOT NULL, content_type TEXT NOT NULL, file_size INTEGER NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, uploaded_by TEXT NOT NULL, created_at TIMESTAMP NOT NULL, UNIQUE(guild_id, name) ); CREATE INDEX idx_guild_stickers_guild ON guild_stickers(guild_id); ``` #### user_stickers ```sql CREATE TABLE user_stickers ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, filename TEXT NOT NULL, content_type TEXT NOT NULL, file_size INTEGER NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, created_at TIMESTAMP NOT NULL, UNIQUE(user_id, name) ); CREATE INDEX idx_user_stickers_user ON user_stickers(user_id); ``` ### Limits (configurable in server config) ```toml [limits.emojis] max_per_guild = 150 # default: 150 static + animated combined max_static_per_guild = 100 max_animated_per_guild = 50 max_file_size_kb = 256 # per emoji max_dimension = 128 # px, both width and height [limits.stickers] max_per_guild = 50 max_per_user = 25 max_file_size_kb = 512 # per sticker max_dimension = 512 ``` --- ## Protocol ### New packet types ``` 0x0070 EMOJI_SYNC server → client, full emoji list for guild 0x0071 EMOJI_ADD client → server, admin upload 0x0072 EMOJI_DELETE client → server, admin delete 0x0073 EMOJI_UPDATE server → client, delta update (single emoji added/removed) 0x0074 STICKER_SYNC server → client, full sticker list 0x0075 STICKER_SEND client → server, send sticker in channel 0x0076 STICKER_UPLOAD client → server, upload sticker 0x0077 STICKER_DELETE client → server, delete sticker 0x0078 EMOJI_DATA client → server, request emoji image bytes 0x0079 EMOJI_DATA_RESP server → client, image bytes (lazy-load, not in sync) 0x0080 GIF_SEARCH client → server, proxy search to Tenor/Giphy 0x0081 GIF_TRENDING client → server, trending GIFs ``` ### EMOJI_SYNC (0x0070) Server sends on guild join. Client caches locally. ```json { "guild_id": "uuid", "emojis": [ { "id": "uuid", "name": "kappa", "content_type": "image/png", "is_animated": false, "width": 128, "height": 128, "file_size": 4096 } ] } ``` Image bytes are NOT included — client lazy-loads via `EMOJI_DATA` + `EMOJI_DATA_RESP`. ### EMOJI_ADD (0x0071) Admin uploads new emoji. Requires `MANAGE_EMOJIS` permission. ```json { "guild_id": "uuid", "name": "pogchamp", "data": "" } ``` Server validates: - Name: lowercase alphanumeric + underscore, 2-32 chars - Image: PNG/GIF/WebP, max 256KB, max 128×128 - Count: under guild limit Response: new emoji object (same shape as in EMOJI_SYNC), server broadcasts `EMOJI_UPDATE` to guild. ### EMOJI_DELETE (0x0072) ```json { "guild_id": "uuid", "emoji_id": "uuid" } ``` Server removes file + DB row, broadcasts `EMOJI_UPDATE` with `deleted: true`. ### EMOJI_DATA / EMOJI_DATA_RESP (0x0078/0x0079) Lazy-load pattern — client requests image bytes when first rendering an emoji. Request: ```json { "emoji_id": "uuid" } ``` Response: ```json { "emoji_id": "uuid", "content_type": "image/png", "data": "" } ``` Client caches image bytes on disk in `emoji_cache/{emoji_id}.{ext}`. ### STICKER_SEND (0x0075) Stickers are sent as a separate message type (not inline). The sticker reference is embedded in a chat message. ```json { "channel_id": "uuid", "sticker_id": "uuid", "sticker_type": "guild" // "guild" | "user" } ``` Gateway creates a message with `content_type: "sticker"` and `content: {sticker_id, sticker_type}`. ### GIF_SEARCH (0x0080) Client requests GIF search via server (server proxies to Tenor/Giphy so API key stays server-side). ```json { "query": "cat dancing", "limit": 20, "offset": 0 } ``` Response: ```json { "results": [ { "id": "tenor_12345", "url": "https://media.tenor.com/...", "preview_url": "https://media.tenor.com/.../tiny.gif", "width": 498, "height": 280, "title": "Cat Dancing" } ] } ``` GIF config in server `config.toml`: ```toml [gif] enabled = true provider = "tenor" # "tenor" | "giphy" api_key = "TENOR_API_KEY" max_results = 50 ``` When `enabled = false` the GIF picker is hidden in all clients. --- ## Client Integration ### Message format with custom emoji Messages reference custom emoji as `<:emoji_name:emoji_id>` inline in text content: ``` User: check out this <:kappa:abc123> and <:pog:def456> ``` Client renders by looking up emoji_id in local cache, falling back to lazy-load via `EMOJI_DATA_RESP`. Unicode emoji (`👍`) render as-is via system font. ### Emoji picker New widget: `client/src/ui/chat/emoji_picker.rs` ``` ┌─────────────────────────────┐ │ [Search emoji... 🔍] │ │─────────────────────────────│ │ Favorites │ Custom │ GIF │ ← tabs │────────────┴─────────┴──────│ │ 👍 😂 🔥 💯 🙏 😎 │ │ 🎉 ✨ 😭 💀 👀 🫡 │ │ 😈 🐱 💪 🚀 🫶 🔒 │ │─────────────────────────────│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │Kappa│ │Pog │ │Hmm │ │Clap│ │ ← custom (rendered as images) │ └───┘ └───┘ └───┘ └───┘ │ └─────────────────────────────┘ ``` ### Sticker panel New widget: `client/src/ui/chat/sticker_panel.rs` - Grid of sticker thumbnails - Tabs: Guild stickers / My stickers - Click to send as sticker message (not inline) - Send button shows sticker preview before sending ### GIF picker New widget: `client/src/ui/chat/gif_picker.rs` - Search bar at top - Trending grid when empty query - Results grid with animated previews - Click to send GIF as embedded image in chat message ### Local cache ``` %APPDATA%/vnox/cache/ ├── emojis/{emoji_id}.{ext} # custom emoji images ├── stickers/{sticker_id}.{ext} # sticker images └── gifs/{gif_id}.gif # recently used GIFs (LRU, max 50) ``` Cache invalidation: client re-requests on version mismatch (server increments `emoji_version` per guild). --- ## Admin UX ### Permissions | Permission | Bit | Description | | ----------------- | --- | --------------------------- | | MANAGE_EMOJIS | 12 | Upload/delete guild emojis | | MANAGE_STICKERS | 13 | Upload/delete guild stickers| These fit into the existing u128 permission bitmask (bits 12-13 are available). ### Emoji management panel Accessible from guild settings (admin only): ``` ┌──────────────────────────────────────────────────┐ │ Guild Emojis [Upload] │ │──────────────────────────────────────────────────│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────┐│ │ │Kappa │ │ Pog │ │ Hmm │ │ Clap │ │ (free) ││ │ │ 4KB │ │ 12KB │ │ 8KB │ │ 16KB │ │ ││ │ │ [✕] │ │ [✕] │ │ [✕] │ │ [✕] │ │ ││ │ └──────┘ └──────┘ └──────┘ └──────┘ └────────┘│ │──────────────────────────────────────────────────│ │ 4/150 emojis used [Save order]│ └──────────────────────────────────────────────────┘ ``` Upload flow: 1. Admin clicks Upload → file dialog (PNG/GIF/WebP) 2. Client validates: ≤256KB, ≤128×128, valid format 3. Client sends `EMOJI_ADD` with base64 data 4. Server validates + stores + broadcasts `EMOJI_UPDATE` ### Sticker management Same pattern as emoji management, separate tab in guild settings. Personal stickers managed in user settings (no admin required). --- ## Federation Considerations (Phase 3) ### Emoji visibility across nodes When two nodes federate: 1. Each node maintains its own emoji set 2. Guild emoji metadata synced via federation protocol 3. Image bytes fetched on-demand (lazy, same as client) 4. Reference format includes origin node: `<:emoji_name:emoji_id@node>` ### Cross-node sticker send 1. Sticker reference sent as `{sticker_id, origin_node}` 2. Receiving node fetches sticker metadata + image from origin node 3. Caches locally for subsequent renders ### GIF proxy GIF search always goes through the user's home node. The server proxies to Tenor/Giphy — no federation needed for GIF results (they're just URLs). --- ## Migration Path ### Phase 1.3 — Unicode reactions only - `message_reactions` table (already designed) - Unicode emoji picker in client - No custom emoji, no stickers, no GIF ### Phase 2 — Custom emoji - Add `guild_emojis` table - Add packet types 0x0070–0x0073, 0x0078–0x0079 - Client emoji picker with custom tab - Admin upload/delete UI - No federation support yet ### Phase 2 — Stickers - Add `guild_stickers` + `user_stickers` tables - Add packet types 0x0074–0x0077 - Sticker panel in client - New message content_type: "sticker" ### Phase 2 — GIF picker - Add Tenor/Giphy config to gateway - Add packet types 0x0080–0x0081 - GIF picker widget in client - Server-side proxy (API key protection) ### Phase 3 — Federation - Cross-node emoji/sticker sync - `@node` suffix in references