mail UI: dedupe broadcast fan-out and drop in-game admin compose
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m6s
Tests · UI / test (pull_request) Successful in 2m24s

Two issues surfaced once the long-lived dev environment finally
reached the diplomail view:

1. `/sent` returns one row per recipient for broadcast and admin
   fan-outs (so the admin tooling can render the materialised
   audience). The list pane fed all rows into the stand-alone
   bucket, so the `{#each entries as e (entryKey(e))}` key in
   `thread-list.svelte` collapsed to the same `standalone:${id}`
   for every recipient and Svelte 5 aborted the render with
   `each_key_duplicate`. Dedupe stand-alones by `message_id` in
   `buildEntries`.

2. The compose dialog exposed an `admin` kind toggle gated on
   "owner of game". That was a Phase 28 plan decision, but admin
   compose is an operator tool (server admin), not an in-game
   action — every game owner should not be able to broadcast
   admin notifications. Drop the admin option, the audience
   sub-toggles, and the admin path through `submit`. The
   `MailStore.composeAdmin` wrapper and the backend RPC stay so
   the future admin UI can call them.

Vitest covers the fan-out dedup with three rows sharing one
`message_id` collapsing to a single stand-alone entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-16 22:38:59 +02:00
parent 57e6c1d253
commit 2119f825d6
3 changed files with 52 additions and 44 deletions
+27
View File
@@ -113,6 +113,33 @@ describe("MailStore.entries", () => {
expect(entries[0].kind).toBe("standalone");
});
it("collapses fan-out rows for the same broadcast message_id", () => {
// `/sent` returns one row per recipient for broadcast and admin
// sends so the admin tooling can render the materialised
// audience. The list pane must collapse the duplicates by
// message_id, otherwise the {#each} key in `thread-list.svelte`
// repeats and Svelte 5 surfaces `each_key_duplicate`.
const store = new MailStore();
const base = {
messageId: "br-shared",
senderKind: "player" as const,
senderRaceName: "Self",
broadcastScope: "game_broadcast" as const,
createdAt: new Date("2026-05-15T14:00:00Z"),
};
store.sent = [
makeMessage({ ...base, recipientRaceName: "Greys" }),
makeMessage({ ...base, recipientRaceName: "Bajori" }),
makeMessage({ ...base, recipientRaceName: "Romulans" }),
];
const entries = store.entries;
expect(entries.length).toBe(1);
expect(entries[0].kind).toBe("standalone");
const standalone = entries[0] as MailStandalone;
expect(standalone.message.messageId).toBe("br-shared");
});
it("counts unread incoming messages in unreadCount", () => {
const store = new MailStore();
store.inbox = [