Files
galaxy-game/ui/docs/diplomail-ui.md
T
Ilia Denisov c48bc83890
Tests · UI / test (pull_request) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m4s
Phase 28 (Step 10): docs — diplomail UI topic + FUNCTIONAL mirror
- `ui/docs/diplomail-ui.md`: new topic doc covering the wire
  surface, recipient-by-race-name decision, threading model,
  translation toggle, push events, badge, layout, and
  accessibility.
- `docs/FUNCTIONAL.md` §11.4 grows a paragraph that records the
  UI's per-race threading rule, the absent read-receipt UX, and
  the recipient-by-race-name compose path. Mirrored verbatim into
  `docs/FUNCTIONAL_ru.md`.
- `ui/PLAN.md` Phase 28 marked done with a "Decisions during
  stage" block matching the implementation plan, and the artifact
  list updated to the actual file set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:48:16 +02:00

4.2 KiB

In-game diplomatic mail UI

Phase 28 wires the in-game mail view that consumes the diplomail subsystem in the backend. The route lives at /games/:id/mail (registered in Phase 10) and replaces the active view when the user opens the "diplomatic mail" entry in the header menu.

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; the gateway translation lives in 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 (the Phase 22 projection of 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 (per Phase 28 decisions): 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 layout (routes/games/[id]/+layout.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 with a "view" deep-link to /games/:id/mail.

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 <select> so screen-readers and keyboard users get the standard semantics.
  • The reply box and the compose body are real <textarea>s so shift-enter newlines, paste, and selection behave correctly.