Phase 28 (Step 3a): /sent returns full message detail per recipient
Tests · Go / test (push) Successful in 2m5s
Tests · Go / test (pull_request) Successful in 2m10s
Tests · Integration / integration (pull_request) Successful in 1m54s
Tests · UI / test (pull_request) Successful in 2m53s

Phase 28's in-game mail UI threads sent messages by the recipient
race name, so the bulk `/sent` endpoint now returns the same
`UserMailMessageDetail` shape as `/inbox` — single sends contribute
one row per message, broadcasts contribute one row per addressee
and the UI collapses them by `message_id` into a stand-alone item.

- `Store.ListSent` / `Service.ListSent` switched from `[]Message`
  to `[]InboxEntry`. SQL grows an INNER JOIN with
  `diplomail_recipients`.
- Handler emits `userMailMessageDetailWire` items; the deprecated
  `userMailSentSummaryWire` is removed.
- `openapi.yaml`: `UserMailSentList.items` now reference
  `UserMailMessageDetail`; the standalone `UserMailSentSummary`
  schema is dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-15 22:27:39 +02:00
parent fed282f2d2
commit 57d2286f5e
4 changed files with 50 additions and 85 deletions
+10 -33
View File
@@ -194,9 +194,9 @@ func (h *UserMailHandlers) Sent() gin.HandlerFunc {
respondDiplomailError(c, h.logger, "user mail sent", ctx, err)
return
}
out := userMailSentListWire{Items: make([]userMailSentSummaryWire, 0, len(items))}
for _, m := range items {
out.Items = append(out.Items, mailMessageSummaryToWire(m))
out := userMailSentListWire{Items: make([]userMailMessageDetailWire, 0, len(items))}
for _, entry := range items {
out.Items = append(out.Items, mailMessageDetailToWire(entry, false))
}
c.JSON(http.StatusOK, out)
}
@@ -555,27 +555,18 @@ type userMailMessageDetailWire struct {
Translator *string `json:"translator,omitempty"`
}
// userMailSentSummaryWire mirrors the response shape for the
// sender-side listing. Recipient state is intentionally omitted (one
// author may have N recipients per broadcast in later stages).
type userMailSentSummaryWire struct {
MessageID string `json:"message_id"`
GameID string `json:"game_id"`
GameName string `json:"game_name,omitempty"`
Kind string `json:"kind"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
BodyLang string `json:"body_lang"`
BroadcastScope string `json:"broadcast_scope"`
CreatedAt string `json:"created_at"`
}
type userMailInboxListWire struct {
Items []userMailMessageDetailWire `json:"items"`
}
// userMailSentListWire mirrors the response shape for the
// sender-side listing. Phase 28's in-game UI threads sent messages
// by the recipient's race name, so the wire carries the full
// message detail (including the recipient snapshot) — single sends
// contribute one row per message, broadcasts contribute one row per
// addressee and the UI collapses them by `message_id`.
type userMailSentListWire struct {
Items []userMailSentSummaryWire `json:"items"`
Items []userMailMessageDetailWire `json:"items"`
}
type userMailUnreadCountWire struct {
@@ -643,20 +634,6 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
return out
}
func mailMessageSummaryToWire(m diplomail.Message) userMailSentSummaryWire {
return userMailSentSummaryWire{
MessageID: m.MessageID.String(),
GameID: m.GameID.String(),
GameName: m.GameName,
Kind: m.Kind,
Subject: m.Subject,
Body: m.Body,
BodyLang: m.BodyLang,
BroadcastScope: m.BroadcastScope,
CreatedAt: m.CreatedAt.UTC().Format(timestampLayout),
}
}
// mailRecipientStateToWire renders the recipient row after a
// mark-read or soft-delete call. The caller only needs the per-user
// state, not the full message body again.