diff --git a/agents-docs/FEATURES.md b/agents-docs/FEATURES.md index c7529d9..28de64a 100644 --- a/agents-docs/FEATURES.md +++ b/agents-docs/FEATURES.md @@ -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//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. diff --git a/agents-docs/LESSONS.md b/agents-docs/LESSONS.md index 122f941..d6ec119 100644 --- a/agents-docs/LESSONS.md +++ b/agents-docs/LESSONS.md @@ -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 `` 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`. +