# In-game diplomatic mail UI The in-game mail view consumes the `diplomail` subsystem in the backend. It is the `mail` active view (`activeView.view === "mail"`) and replaces the active view when the user opens the "diplomatic mail" entry in the header menu (`activeView.select("mail")`). The app-shell has no per-view URL — see [`navigation.md`](navigation.md). ## Wire surface Eight ConnectRPC commands sit between UI and backend, all under the `user.games.mail.*` namespace: | Command | Backend REST endpoint | |---|---| | `user.games.mail.inbox` | `GET /api/v1/user/games/{id}/mail/inbox` | | `user.games.mail.sent` | `GET …/mail/sent` | | `user.games.mail.message.get` | `GET …/mail/messages/{message_id}` | | `user.games.mail.send` | `POST …/mail/messages` | | `user.games.mail.broadcast` | `POST …/mail/broadcast` | | `user.games.mail.admin` | `POST …/mail/admin` | | `user.games.mail.read` | `POST …/mail/messages/{id}/read` | | `user.games.mail.delete` | `DELETE …/mail/messages/{id}` | The FlatBuffers schemas live under [`pkg/schema/fbs/diplomail.fbs`](../../pkg/schema/fbs/diplomail.fbs); the gateway translation lives in [`gateway/internal/backendclient/mail_commands.go`](../../gateway/internal/backendclient/mail_commands.go). ## Recipient by race name The compose flow does **not** consult a memberships listing. The recipient picker reads `gameState.report.races[].name` (projected from `report.player[]`), and the send request carries the chosen race name as `recipient_race_name`. The backend resolves it against `Memberships.ListMembers(gameID, "active")` and rejects with `forbidden` if the matching member is no longer active. This keeps the UI off the lobby surface for the common case. ## Threading model `MailStore.entries` is the derived rune the active view consumes. It projects the union of inbox and sent into: - **Per-race threads** — every personal message keyed by another race contributes to a thread keyed on that race name. Incoming is keyed on `sender_race_name`; outgoing is keyed on `recipient_race_name`. Thread messages are sorted oldest → newest for chat-style rendering; the unread badge counts incoming `read_at === null` rows only. - **Stand-alone items** — system mail (`sender_kind=system`), admin notifications (`sender_kind=admin`), and the caller's own paid-tier broadcasts (`broadcast_scope=game_broadcast`). Backend returns one row per recipient for paid-tier broadcasts; the UI collapses them by `message_id` into a single stand-alone item. `read_at` and `deleted_at` are not surfaced to the user in any pane — they only drive the badge counter and the optimistic mark-read state. This is intentional: the user-facing spec for diplomatic mail does not promise read receipts. ## Translation toggle When a message detail carries `translated_body`, the body and (if non-empty) subject default to the translated rendering. Each message pane exposes a "Show original" / "Show translation" button that flips the per-message state. Messages without a cached translation render the original directly with no toggle. ## Push events `diplomail.message.received` push frames are dispatched from `api/events.svelte.ts` via the singleton SubscribeEvents stream. The in-game shell (`lib/game/game-shell.svelte`) parses the verified payload, calls `mailStore.applyPushEvent(gameId)` (which re-fetches the inbox — the payload only carries a preview), and raises a toast through `lib/toast.svelte.ts` whose "view" action switches to the mail view in memory (`activeView.select("mail")`) — no URL navigation. The header view-menu's mail entry shows `mailStore.unreadCount` as an inline pill — the only chrome the badge needs. ## Layout Desktop (≥ 768 px) renders a two-pane CSS grid: list on the left, detail on the right. Mobile flips to a single-pane stack; tapping a list row hides the list and shows the detail with a back button. ## Accessibility - Bodies render through Svelte's default text-content path (no HTML parsing) per the backend rule of treating message text as plain UTF-8. - The compose dialog uses native form controls; the recipient picker is a `