# Chat Domain Text messaging, reactions, GIF search, typing indicators, and the user list. All UI is under `feature/`; application services handle GIF integration; domain rules govern message editing, deletion, and sync. ## Module map ``` chat/ ├── application/ │ └── klipy.service.ts GIF search via the KLIPY API (proxied through the server) │ ├── domain/ │ ├── message.rules.ts canEditMessage, normaliseDeletedMessage, getMessageTimestamp │ └── message-sync.rules.ts Inventory-based sync: chunkArray, findMissingIds, limits │ ├── feature/ │ ├── chat-messages/ Main chat view (orchestrates composer, list, overlays) │ │ ├── chat-messages.component.ts Root component: replies, GIF picker, reactions, drag-drop │ │ ├── components/ │ │ │ ├── message-composer/ Markdown toolbar, file drag-drop, send │ │ │ ├── message-item/ Single message bubble with edit/delete/react │ │ │ ├── message-list/ Paginated list (50 msgs/page), auto-scroll, Prism highlighting │ │ │ └── message-overlays/ Context menus, reaction picker, reply preview │ │ ├── models/ View models for messages │ │ └── services/ │ │ └── chat-markdown.service.ts Markdown-to-HTML rendering │ │ │ ├── klipy-gif-picker/ GIF search/browse picker panel │ ├── typing-indicator/ "X is typing..." display (3 s TTL, max 4 names) │ └── user-list/ Online user sidebar │ └── index.ts Barrel exports ``` ## Component composition `ChatMessagesComponent` is the root of the chat view. It renders the message list, composer, and overlays as child components and coordinates cross-cutting interactions like replying to a message or inserting a GIF. ```mermaid graph TD Chat[ChatMessagesComponent] List[MessageListComponent] Composer[MessageComposerComponent] Overlays[MessageOverlays] Item[MessageItemComponent] GIF[KlipyGifPickerComponent] Typing[TypingIndicatorComponent] Users[UserListComponent] Chat --> List Chat --> Composer Chat --> Overlays Chat --> GIF List --> Item Item --> Overlays click Chat "feature/chat-messages/chat-messages.component.ts" "Root chat view" _blank click List "feature/chat-messages/components/message-list/" "Paginated message list" _blank click Composer "feature/chat-messages/components/message-composer/" "Markdown toolbar + send" _blank click Overlays "feature/chat-messages/components/message-overlays/" "Context menus, reaction picker" _blank click Item "feature/chat-messages/components/message-item/" "Single message bubble" _blank click GIF "feature/klipy-gif-picker/" "GIF search panel" _blank click Typing "feature/typing-indicator/" "Typing indicator" _blank click Users "feature/user-list/" "Online user sidebar" _blank ``` ## Message lifecycle Messages are created in the composer, broadcast to peers over the data channel, and rendered in the list. Editing and deletion are sender-only operations. ```mermaid sequenceDiagram participant User participant Composer as MessageComposer participant Store as NgRx Store participant DC as Data Channel participant Peer as Remote Peer User->>Composer: Type + send Composer->>Store: dispatch addMessage Composer->>DC: broadcastMessage(chat-message) DC->>Peer: chat-message event Note over User: Edit User->>Store: dispatch editMessage User->>DC: broadcastMessage(edit-message) Note over User: Delete User->>Store: dispatch deleteMessage (normaliseDeletedMessage) User->>DC: broadcastMessage(delete-message) ``` ## Text channel scoping `ChatMessagesComponent` renders only the active text channel selected in `store/rooms`. Legacy messages without an explicit `channelId` are treated as `general` for backward compatibility, while new sends and typing events attach the active `channelId` so one text channel does not leak state into the rest of the server. Voice channels live in the same server-owned channel list, but they do not participate in chat-message routing. If a room has no text channels, the room shell in `features/room/chat-room/` renders an empty state instead of mounting the chat view. The chat domain only mounts once a valid text channel exists. ## Message sync When a peer connects (or reconnects), both sides exchange an inventory of their recent messages so each can request anything it missed. The inventory is capped at 1 000 messages and sent in chunks of 200. ```mermaid sequenceDiagram participant A as Peer A participant B as Peer B A->>B: inventory (up to 1000 msg IDs + timestamps) B->>B: findMissingIds(remote, local) B->>A: request missing message IDs A->>B: message payloads (chunked, 200/batch) ``` `findMissingIds` compares each remote item's timestamp and reaction/attachment counts against the local map. Any item that is missing, newer, or has different counts is requested. ## GIF integration `KlipyService` checks availability on the active server, then proxies search requests through the server API. Rendered remote images now attempt a direct load first and only fall back to the image proxy after the browser reports a load failure, which is the practical approximation of a CORS or mixed-content fallback path in the renderer. ```mermaid graph LR Picker[KlipyGifPickerComponent] Klipy[KlipyService] SD[ServerDirectoryFacade] API[Server API] Picker --> Klipy Klipy --> SD Klipy --> API click Picker "feature/klipy-gif-picker/" "GIF search panel" _blank click Klipy "application/klipy.service.ts" "GIF search via KLIPY API" _blank click SD "../server-directory/application/server-directory.facade.ts" "Resolves API base URL" _blank ``` ## Domain rules | Function | Purpose | |---|---| | `canEditMessage(msg, userId)` | Only the sender can edit their own message | | `normaliseDeletedMessage(msg)` | Strips content and reactions from deleted messages | | `getMessageTimestamp(msg)` | Returns `editedAt` if present, otherwise `timestamp` | | `getLatestTimestamp(msgs)` | Max timestamp across a batch, used for sync ordering | | `chunkArray(items, size)` | Splits arrays into fixed-size chunks for batched transfer | | `findMissingIds(remote, local)` | Compares inventories and returns IDs to request | ## Typing indicator `TypingIndicatorComponent` listens for typing events from peers scoped to the current server and active text channel. Each event resets a 3-second TTL timer for that channel. If no new event arrives within 3 seconds, the user is removed from the typing list. At most 4 names are shown; beyond that it displays "N users are typing".