Direct Message Domain
Direct messages provide local, offline-safe one-to-one and small-group messaging over the existing WebRTC data channel, with a signaling relay fallback when no peer data channel is available but a route to the recipient is known.
The same PeerDeliveryService also exposes direct-call events for the direct-call domain so private calls can ring through either an open peer data channel or a known signaling route without adding a second recipient lookup path.
Structure
direct-message/
├── application/services/ DirectMessageService, OfflineMessageQueueService, FriendService, PeerDeliveryService
├── domain/ Direct message models and status-transition rules
├── infrastructure/ User-scoped local repositories
└── feature/ DM rail, chat view, message rows, user search, friend button
Flow
DirectMessageService.sendMessage()stores the message locally withQUEUED.PeerDeliveryServicetries to send adirect-messageP2P event to every other participant's current peer id.- If no data channel is connected,
PeerDeliveryServicetries each participant's known signaling route before leaving the message queued. - If either transport sends, the sender advances to
SENT; otherwise the message id remains inOfflineMessageQueueService. - The recipient persists the message as
DELIVEREDand sends adirect-message-statusevent back. - Opening the conversation marks incoming messages as
ACKNOWLEDGEDand emits a status event.
Incoming PM and group-chat events are ignored unless the current user is declared in the message recipients, participant profiles, or existing local conversation. Sync requests are only answered for conversation participants, so a stray peer route cannot create unread state or expose private history.
Status transitions are monotonic, so a stale SENT event cannot overwrite DELIVERED or ACKNOWLEDGED.
Chat View
The DM view reuses the chat domain's shared message list, composer, overlays, markdown renderer, link embeds, media players, and attachment controls. Direct-message records are mapped into the shared Message shape at the feature boundary so PMs keep the same date separators, replies, editing, deletion, reactions, image lightbox, audio playback, and video playback as server text channels.
Message edits, deletions, and reaction changes are stored locally and mirrored to the peer with direct-message-mutation events. Delivery state remains direct-message-owned and is exposed separately from the visible shared chat row UI.
When a private call grows beyond two participants, the direct-call domain creates a new empty group conversation and points the call chat panel at it. The previous one-to-one conversation remains untouched, so private history is not copied into the group chat. Group conversations reuse the same composer, message list, attachment, GIF, markdown, link-embed, typing, mutation, and sync paths as one-to-one DMs; delivery simply fans out to every participant except the sender.
The DM header and conversation list can start calls from both one-to-one and group conversations. Group calls reuse the group conversation id as the call id and send the same ring notification to every other participant.
Typing state is DM-owned as well. The composer emits direct-message-typing events, and the chat view renders the active peer names with a short TTL so the embedded private-call chat has the same typing feedback as a standalone PM.
When a conversation opens, a peer reconnects, or network service is restored, the selected conversation requests a bounded direct-message-sync snapshot from the peer. Incoming snapshots merge the newest messages by id instead of replacing local history, which lets clients backfill older PMs when their local stores drift.
GIFs
The DM composer reuses the chat domain's KLIPY integration. Availability and GIF search go through the configured signal server API, and selected GIFs are sent as markdown image messages so the same proxy-fallback image rendering path is used in DMs and server chat.
Avatars
Conversation participants keep avatar/profile metadata captured from user cards or room membership. When a PM is opened and the peer avatar is missing, the view asks the peer for the existing profile-avatar sync payload so downloaded user icons can be filled in without adding a DM-specific avatar transport.
Persistence
Repositories are user-scoped and stored locally under metoyou_direct_message_* keys. The storage is intentionally domain-owned so browser and Electron runtimes share the same renderer API without changing the existing chat-message database tables.