VNOX-Client/docs/05-features/custom-emoji-stickers.md

443 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "<base64>"
}
```
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": "<base64>"
}
```
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