Phase 28: diplomatic mail UI (work in progress) #11
@@ -316,10 +316,13 @@ func (s *Service) allowedKinds(ctx context.Context, gameID, userID uuid.UUID) (m
|
||||
return map[string]bool{KindAdmin: true}, nil
|
||||
}
|
||||
|
||||
// ListSent returns personal messages authored by senderUserID in
|
||||
// gameID, newest first. Admin/system rows have no `sender_user_id`
|
||||
// and are therefore excluded; the user surface does not need them.
|
||||
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
|
||||
// ListSent returns the sender-side view of personal messages
|
||||
// authored by senderUserID in gameID, newest first. Each entry pairs
|
||||
// the message with one of its recipient rows; single sends contribute
|
||||
// one entry per message, broadcasts contribute one entry per
|
||||
// addressee. Admin and system rows have no `sender_user_id` and are
|
||||
// therefore excluded; the user surface does not need them.
|
||||
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
|
||||
return s.deps.Store.ListSent(ctx, gameID, senderUserID)
|
||||
}
|
||||
|
||||
|
||||
@@ -243,25 +243,38 @@ func (s *Store) ListInbox(ctx context.Context, gameID, userID uuid.UUID) ([]Inbo
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListSent returns messages authored by senderUserID in gameID,
|
||||
// newest first. Personal messages only — admin/system rows have
|
||||
// `sender_user_id IS NULL` and are filtered out by the WHERE clause.
|
||||
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
|
||||
// ListSent returns the sender-side view of personal messages
|
||||
// authored by senderUserID in gameID, newest first. Each
|
||||
// `InboxEntry` carries the message together with one of its
|
||||
// recipient rows — single sends produce one entry per message;
|
||||
// game broadcasts produce one entry per addressee (the in-game
|
||||
// mail UI collapses broadcast entries into a single stand-alone
|
||||
// item by `message_id`). Admin / system rows have
|
||||
// `sender_user_id IS NULL` and are excluded by the WHERE clause.
|
||||
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
|
||||
m := table.DiplomailMessages
|
||||
stmt := postgres.SELECT(messageColumns()).
|
||||
FROM(m).
|
||||
r := table.DiplomailRecipients
|
||||
cols := append(messageColumns(), recipientColumns()...)
|
||||
stmt := postgres.SELECT(cols).
|
||||
FROM(m.INNER_JOIN(r, r.MessageID.EQ(m.MessageID))).
|
||||
WHERE(
|
||||
m.GameID.EQ(postgres.UUID(gameID)).
|
||||
AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))),
|
||||
).
|
||||
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC())
|
||||
var rows []model.DiplomailMessages
|
||||
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
|
||||
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC(), r.RecipientID.ASC())
|
||||
var dest []struct {
|
||||
model.DiplomailMessages
|
||||
Recipient model.DiplomailRecipients `alias:"diplomail_recipients"`
|
||||
}
|
||||
if err := stmt.QueryContext(ctx, s.db, &dest); err != nil {
|
||||
return nil, fmt.Errorf("diplomail store: list sent %s/%s: %w", gameID, senderUserID, err)
|
||||
}
|
||||
out := make([]Message, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, messageFromModel(row))
|
||||
out := make([]InboxEntry, 0, len(dest))
|
||||
for _, row := range dest {
|
||||
out = append(out, InboxEntry{
|
||||
Message: messageFromModel(row.DiplomailMessages),
|
||||
Recipient: recipientFromModel(row.Recipient),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+8
-36
@@ -4400,41 +4400,6 @@ components:
|
||||
translator:
|
||||
type: string
|
||||
description: Identifier of the translation engine that produced the cached row.
|
||||
UserMailSentSummary:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- message_id
|
||||
- game_id
|
||||
- kind
|
||||
- body
|
||||
- body_lang
|
||||
- broadcast_scope
|
||||
- created_at
|
||||
properties:
|
||||
message_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_name:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum: [personal, admin]
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
body_lang:
|
||||
type: string
|
||||
broadcast_scope:
|
||||
type: string
|
||||
enum: [single, game_broadcast, multi_game_broadcast]
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
UserMailInboxList:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -4445,6 +4410,13 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/UserMailMessageDetail"
|
||||
UserMailSentList:
|
||||
description: |
|
||||
Sender-side listing of personal messages authored by the
|
||||
caller. Each item carries the same shape as inbox entries
|
||||
(including the recipient snapshot); single sends contribute
|
||||
one row per message, broadcasts contribute one row per
|
||||
addressee so the in-game UI can collapse them by
|
||||
`message_id` into a single stand-alone item.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [items]
|
||||
@@ -4452,7 +4424,7 @@ components:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/UserMailSentSummary"
|
||||
$ref: "#/components/schemas/UserMailMessageDetail"
|
||||
UserMailUnreadCount:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
||||
Reference in New Issue
Block a user