feat: Add emoji and alot of other fixes
This commit is contained in:
@@ -8,6 +8,7 @@ It must stay accurate as new features are introduced, renamed, merged, or remove
|
||||
|
||||
## Feature list (alphabetical)
|
||||
|
||||
- [Custom Emoji](features/custom-emoji.md) — peer-synced user-created emoji assets, chat reaction shortcuts, and composer emoji insertion.
|
||||
- [Server Discovery](features/server-discovery.md) — featured/trending public-server REST endpoints (server) consumed by the `/dashboard` and `/servers` client pages.
|
||||
|
||||
The product client already documents its bounded contexts at `toju-app/src/app/domains/<name>/README.md` (Access Control, Attachment, Authentication, Chat, Direct Call, Direct Message, Experimental Media, Game Activity, Notifications, Plugins, Profile Avatar, Screen Share, Server Directory, Theme, Voice Connection, Voice Session). Those domain READMEs cover internal product-client behavior.
|
||||
|
||||
@@ -25,6 +25,34 @@ Durable rules for AI agents working on this project. Read this file at session s
|
||||
|
||||
## Lessons
|
||||
|
||||
### Use dense arrays for chunked transfer buffers [custom-emoji] [webrtc]
|
||||
|
||||
- **Trigger:** chunked P2P asset assembly marks a transfer complete after the first chunk because `array.some()` skips sparse holes created by `new Array(total)`.
|
||||
- **Rule:** initialize chunk buffers with `Array.from({ length: total }, () => undefined)` (or another dense initializer) before using `some`/`every`/`filter` to detect completion.
|
||||
- **Why:** a single assigned slot in a sparse array makes `.some((chunk) => !chunk)` return false, so multi-chunk custom emoji transfers are dropped and peers never receive uploaded images larger than one chunk.
|
||||
- **Example:** `CustomEmojiService.receiveTransferStart` stores `chunks: Array.from({ length: total }, () => undefined)` instead of `new Array(total)`.
|
||||
|
||||
### Route custom emoji right-click through the native context menu [custom-emoji] [ux]
|
||||
|
||||
- **Trigger:** adding a second emoji-specific context menu beside `NativeContextMenuComponent`, or attaching handlers only to `<img>` nodes.
|
||||
- **Rule:** mark emoji hosts with `data-custom-emoji` / `data-custom-emoji-library` plus `data-custom-emoji-id`, let `NativeContextMenuComponent` own add/remove actions, and use a capture-phase `preventDefault` so Electron/browser image menus do not override them.
|
||||
- **Why:** the shell context menu already intercepts every image right-click; duplicate menus fight each other and button/div wrappers miss img-only handlers.
|
||||
- **Example:** reaction pills and picker buttons carry the data attributes; `resolveCustomEmojiContextMenuTarget()` opens **Add to emoji library** / **Remove from emoji library** from the global menu.
|
||||
|
||||
### Separate known emoji assets from saved library [custom-emoji] [ux]
|
||||
|
||||
- **Trigger:** syncing remote custom emoji directly into the picker/library when it is first seen in chat.
|
||||
- **Rule:** store remote emoji as known renderable assets, but only show them in the user's picker after an explicit save action such as right-clicking the rendered emoji.
|
||||
- **Why:** users need messages to render, but they should control which seen emoji become part of their local emoji library.
|
||||
- **Example:** `CustomEmojiService.emojis` filters to saved emoji, while `findEmoji(id)` can still resolve unsaved known assets for message rendering.
|
||||
|
||||
### Chunk custom emoji assets over data channels [custom-emoji] [webrtc]
|
||||
|
||||
- **Trigger:** sending uploaded custom emoji image data through a single `custom-emoji-full` peer event.
|
||||
- **Rule:** stream custom emoji assets as a metadata envelope plus bounded `custom-emoji-chunk` events; use buffered sends for back-pressure, but never rely on buffering to make oversized messages safe.
|
||||
- **Why:** a single base64 data URL can exceed browser SCTP message limits and fire `RTCDataChannel.onerror`, breaking the app-wide chat channel.
|
||||
- **Example:** send `{ type: 'custom-emoji-full', customEmojiTransfer, total }`, then `custom-emoji-chunk` events with small `data` slices.
|
||||
|
||||
### Re-clear visible notification channels after recompute [notifications] [startup]
|
||||
|
||||
- **Trigger:** fixing startup unread badges by only changing read-marker writes or initial hydration.
|
||||
@@ -60,6 +88,20 @@ Durable rules for AI agents working on this project. Read this file at session s
|
||||
- **Why:** `npm run test` only runs the toju-app Vitest suite — it doesn't cover the server, Electron, or website packages. ESLint (flat config in `eslint.config.js`) is the universal check across every package; type-style violations slip through tests and break Gitea Workflows for the next agent.
|
||||
- **Example:** `npm run lint && echo OK` — only claim done after seeing `OK`. For Electron type errors specifically, also confirm `npm run build:electron` succeeds (it invokes `tsc -p tsconfig.electron.json`).
|
||||
|
||||
### Resolve Electron drag-and-drop file paths with webUtils [attachments] [electron]
|
||||
|
||||
- **Trigger:** large videos play after drag-and-drop upload, but after restart the uploader sees a peer-download error even though they sent the file from disk.
|
||||
- **Rule:** when accepting dropped or pasted files in Electron, call `webUtils.getPathForFile(file)` from preload (`getPathForFile` on `electronAPI`) and annotate the `File` before `publishAttachments`; never rely on `File.path` in the renderer.
|
||||
- **Why:** Chromium removed direct `File.path` access in modern Electron; without `getPathForFile`, large uploads only exist as in-memory blobs and cannot be copied into app data for reload playback.
|
||||
- **Example:** `annotateLocalFilePath(file, { getPathForFile: electronApi.getPathForFile })` in `ChatMessageComposerComponent.addPendingFiles`.
|
||||
|
||||
### Preserve uploader local attachment paths across sync [attachments] [persistence]
|
||||
|
||||
- **Trigger:** large Electron uploads play from `filePath` after send, but after reload the uploader sees "The connected peers do not have this file right now" and must P2P-download their own file.
|
||||
- **Rule:** never persist synced attachment metadata with `filePath`/`savedPath` stripped — merge with stored local paths, finish attachment DB init before applying sync batches, and try local disk restore before sending `file-request` to peers.
|
||||
- **Why:** P2P sync intentionally omits local-only paths; a startup race can overwrite the uploader's saved `filePath` with `null`, and large videos (>10 MB) are not auto-copied to app data so only the original path can restore playback.
|
||||
- **Example:** copy large Electron uploads into app-data on `publishAttachments`, `mergeAttachmentLocalPaths(incomingMeta, storedRecord)` in `persistAttachmentMeta`, `await persistence.whenReady()` in `registerSyncedAttachments`, and `tryRestoreAttachmentFromLocal()` before any `file-request`.
|
||||
|
||||
<!--
|
||||
Add new lessons above this comment, newest at the top.
|
||||
Delete this example once the project has accumulated 2-3 real lessons.
|
||||
|
||||
64
agents-docs/features/custom-emoji.md
Normal file
64
agents-docs/features/custom-emoji.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Custom Emoji
|
||||
|
||||
> **Area:** custom-emoji
|
||||
> **Status:** Active
|
||||
> **Last updated:** 2026-06-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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **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 with `savedByUser` enabled; saved emoji appear in the picker and shortcut ranking.
|
||||
- **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.
|
||||
|
||||
## Peer Envelope Contract
|
||||
|
||||
Custom emoji uses `ChatEvent` data-channel envelopes:
|
||||
|
||||
- `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.
|
||||
|
||||
## Business Rules
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Data Access
|
||||
|
||||
- Browser runtime stores custom emoji in IndexedDB store `customEmojis`.
|
||||
- Electron runtime stores custom emoji in SQLite table `custom_emojis`, created by migration `1000000000011-AddCustomEmojis`.
|
||||
- Renderer access goes through `DatabaseService` methods `saveCustomEmoji`, `getCustomEmojis`, and `deleteCustomEmoji`.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user