# 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

1. `DirectMessageService.sendMessage()` stores the message locally with `QUEUED`.
2. `PeerDeliveryService` tries to send a `direct-message` P2P event to every other participant's current peer id.
3. If no data channel is connected, `PeerDeliveryService` tries each participant's known signaling route before leaving the message queued.
4. If either transport sends, the sender advances to `SENT`; otherwise the message id remains in `OfflineMessageQueueService`.
5. The recipient persists the message as `DELIVERED` and sends a `direct-message-status` event back.
6. Opening the conversation marks incoming messages as `ACKNOWLEDGED` and 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.
