chore: Fix app

This commit is contained in:
2026-07-14 00:41:05 +02:00
parent 3e090933fd
commit edc4d935d8
98 changed files with 2878 additions and 155 deletions
+101 -40
View File
@@ -2,64 +2,125 @@
> **Area:** custom-emoji
> **Status:** Active
> **Last updated:** 2026-06-05
> **Last updated:** 2026-07-05
## Overview
Custom emoji lets users upload small image emoji, use them in chat messages and reactions, and sync emoji assets needed for rendering to connected peers over the existing data-channel mesh.
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
- Own custom emoji asset validation, local persistence, user-saved library membership, shortcut ranking, and peer-to-peer asset sync.
- Expose a shared picker consumed by chat message reactions and the chat composer.
- Keep usage ranking local to the current user; usage counts are not synced.
- Does not store custom emoji on the signaling server.
- 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.
## Key Concepts
This area does **not** own:
- **Custom emoji asset**: A user-created image stored as a data URL with id, name, mime, size, hash, creator, timestamps, and optional saved-library membership.
- **Known custom emoji**: A synced asset available for message rendering and forwarding, but not shown in the current user's picker unless saved.
- **Saved custom emoji**: A known asset the current user added to their library; saved emoji appear in the picker and shortcut ranking. Library membership is **user-bound, not client-bound** — it is tracked per signed-in user (keyed by user id), so a second account on the same device never inherits the first account's library.
- **Emoji shortcut row**: The seven most-used emoji entries for the current user plus an eighth control that opens the full selector.
- **Custom emoji token**: The stable message/reaction representation `:emoji[id](name)`, resolved locally to the synced image asset when rendering.
- **Composer emoji alias**: The readable inline draft representation `:name:`. The composer rewrites known aliases to stable custom emoji tokens only when sending.
- Message send/edit transport → [messaging.md](messaging.md).
- Profile avatar bytes → `toju-app/src/app/domains/profile-avatar/README.md`.
- Server-side storage (none).
## Peer Envelope Contract
## Key concepts
Custom emoji uses `ChatEvent` data-channel envelopes:
- **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:<userId>`); 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.
- `custom-emoji-summary`: `{ customEmojiSummaries: [{ id, hash, updatedAt }] }`
- `custom-emoji-request`: `{ ids: string[] }`
- `custom-emoji-full`: `{ customEmojiTransfer: Omit<CustomEmoji, 'dataUrl'>, total: number }`
- `custom-emoji-chunk`: `{ customEmojiId, index, total, data }`
---
When a peer connects, each side sends a summary of known assets. The receiver requests missing or stale emoji by id, and the owner replies with a small manifest followed by bounded base64 chunks using buffered peer sends. Creating a new emoji also streams that manifest and chunk sequence to every currently connected peer. Outgoing room chat messages, edits, reactions, and direct messages proactively push every referenced custom emoji asset to connected peers in parallel with the message event, so receivers do not wait for a request round-trip. Small assets that fit under `CUSTOM_EMOJI_INLINE_MAX_JSON_BYTES` travel inline in one `custom-emoji-full` event; larger assets use manifest plus chunks. Incoming chat messages and chat-sync batches still scan for `:emoji[id](name)` tokens and request any missing assets from the sender as a repair path. Full inline `customEmoji` payloads remain accepted for backward compatibility.
## Peer envelope contract (P2P)
## Business Rules
| 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 |
- Uploads are capped at 1 MB.
- Accepted image types match profile avatars: WebP, GIF, JPG, and JPEG.
- Local shortcut ranking is keyed by the active user and includes Unicode emoji plus saved custom emoji only.
- Saved-library membership is bound to the user, not the client: `CustomEmojiService` tracks the set of saved emoji ids per user id in `localStorage` (`metoyou_custom_emoji_saved:<userId>`, mirroring the per-user usage ranking). The picker shows only emoji in the active user's saved set, so signing in as a different account on the same client never exposes the previous account's library. On first load after this change the set is seeded from legacy `savedByUser` rows the user actually created (`creatorUserId === userId`), so creators keep their library while other local accounts stay empty.
- Message rendering reserves inline emoji space with a transparent placeholder image while a referenced custom emoji asset is not yet available; deferred markdown placeholders rewrite tokens to readable `:name:` aliases so raw `:emoji[id](name)` text never flashes in chat.
- Seen custom emoji are not added to the picker automatically; right-click a rendered custom emoji in chat or on a custom emoji reaction and choose **Add to emoji library** from the app context menu (`NativeContextMenuComponent`).
- Saved custom emoji can be removed from the picker library by right-clicking them inside the emoji picker and choosing **Remove from emoji library**; the asset stays available for rendering messages that already reference it.
- Emoji hosts are marked with `data-custom-emoji` / `data-custom-emoji-library` plus `data-custom-emoji-id` so the global context menu can distinguish them from regular images and suppress the default **Copy Image** action.
- The full emoji picker includes a search field that filters built-in Unicode emoji by common terms and saved custom emoji by name.
- Custom emoji data-channel chunks are capped below typical SCTP message limits; back-pressure alone is not enough because a single oversized send can fire `RTCDataChannel.onerror`.
- Completed transfers are persisted only when the reconstructed data URL matches the manifest size and hash; corrupt local rows are dropped before summaries are advertised.
**Handshake:** on peer connect both sides send summaries; receiver requests stale/missing ids; owner sends manifest then chunked payloads via buffered sends.
## Data Access
**Proactive push:** outgoing chat/DM messages scan for tokens and push assets to connected peers in parallel with the message event.
- Browser runtime stores custom emoji image assets in IndexedDB store `customEmojis` (per-user database scope).
- Electron runtime stores custom emoji image assets in SQLite table `custom_emojis`, created by migration `1000000000011-AddCustomEmojis` (a single shared desktop database).
- Renderer access goes through `DatabaseService` methods `saveCustomEmoji`, `getCustomEmojis`, and `deleteCustomEmoji`. These persist the image **assets** only; they are not scoped per user (the Electron table is shared across local accounts). Per-user **library membership** lives separately in `localStorage` (`metoyou_custom_emoji_saved:<userId>`), which is what keeps the picker user-bound even on a shared client database.
**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:<userId>` |
| Electron | SQLite `custom_emojis` (shared desktop DB) | same localStorage key |
| Capacitor | SQLite `custom_emojis` in `metoyou__<userId>` | 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
- Unit tests cover upload size validation, shortcut selection, picker search filtering, custom emoji token generation, data-channel chunk splitting, readable composer alias rewriting, transfer integrity, saved-library membership, and add/remove library context-menu actions.
- `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
---
- Emoji payloads are image-only and size-limited before persistence or broadcast.
- Assets sync only to already connected peers; the signaling server does not persist or proxy emoji images.
## 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 |