Files
Toju/toju-app/src/app/domains/chat/README.md

7.0 KiB

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.

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.

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.

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.

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".