feat: sparser robot nudges, typed unread badge, lobby unread bump
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
Three owner-requested polish changes: - robot: replace the lengthening 60-90 min -> 6 h proactive-nudge ramp with a flat uniform 9-12 h wait before every nudge; the existing sleep-window gate still skips and defers a nudge that would land in the robot's night. - ui: colour the lobby/in-game unread dot by type -- the regular danger colour when a chat message is unread, a softer amber (--warn) when only nudges are. Adds a per-viewer unread_messages flag (chat_messages.kind='message') across the backend DTO, FlatBuffers wire, gateway transcode and the UI store. - ui: float games with any unread notification to the top of the lobby's your-turn and opponent-turn sections (finished keeps its order), reusing the existing unread_chat flag. Docs (ARCHITECTURE 7, FUNCTIONAL + _ru) updated. No DB migration; the new wire field is backward-compatible.
This commit is contained in:
@@ -67,6 +67,21 @@ func (svc *Service) HasUnread(ctx context.Context, gameID, viewerID uuid.UUID) (
|
||||
return svc.store.hasUnread(ctx, gameID, viewerID)
|
||||
}
|
||||
|
||||
// UnreadMessageGames returns the subset of UnreadGames whose unread entries include a real chat
|
||||
// message (a 'message' kind, not a nudge), for colouring the lobby's per-card unread badge: a game
|
||||
// present here has an unread message (the regular badge), one only in UnreadGames has nudges alone
|
||||
// (the soft nudge badge).
|
||||
func (svc *Service) UnreadMessageGames(ctx context.Context, viewerID uuid.UUID) (map[uuid.UUID]bool, error) {
|
||||
return svc.store.unreadMessageGames(ctx, viewerID)
|
||||
}
|
||||
|
||||
// HasUnreadMessage reports whether viewerID has an unread chat message (a 'message' kind, not a
|
||||
// nudge) in the game, for distinguishing a message badge from a nudge-only one on a single game's
|
||||
// state and move-result views.
|
||||
func (svc *Service) HasUnreadMessage(ctx context.Context, gameID, viewerID uuid.UUID) (bool, error) {
|
||||
return svc.store.hasUnreadMessage(ctx, gameID, viewerID)
|
||||
}
|
||||
|
||||
// readMark is one chat entry that flipped from unread to read, carrying what the
|
||||
// publish-to-read latency metric needs.
|
||||
type readMark struct {
|
||||
@@ -161,6 +176,43 @@ func (s *Store) hasUnread(ctx context.Context, gameID, viewerID uuid.UUID) (bool
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// unreadMessageGames is unreadGames restricted to real chat messages (kind 'message', not a
|
||||
// nudge), so the caller can tell a message badge from a nudge-only one in one extra query.
|
||||
func (s *Store) unreadMessageGames(ctx context.Context, viewerID uuid.UUID) (map[uuid.UUID]bool, error) {
|
||||
const q = `SELECT DISTINCT m.game_id
|
||||
FROM backend.chat_messages m
|
||||
JOIN backend.game_players p ON p.game_id = m.game_id AND p.account_id = $1
|
||||
WHERE m.kind = 'message' AND m.unread_seats <> 0 AND (m.unread_seats::int & (1 << p.seat::int)) <> 0`
|
||||
rows, err := s.db.QueryContext(ctx, q, viewerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("social: unread message games: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[uuid.UUID]bool)
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, fmt.Errorf("social: scan unread message games: %w", err)
|
||||
}
|
||||
out[id] = true
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// hasUnreadMessage reports whether viewerID has an unread real chat message (kind 'message', not
|
||||
// a nudge) in the one game.
|
||||
func (s *Store) hasUnreadMessage(ctx context.Context, gameID, viewerID uuid.UUID) (bool, error) {
|
||||
const q = `SELECT EXISTS(
|
||||
SELECT 1 FROM backend.chat_messages m
|
||||
JOIN backend.game_players p ON p.game_id = m.game_id AND p.account_id = $2
|
||||
WHERE m.game_id = $1 AND m.kind = 'message' AND (m.unread_seats::int & (1 << p.seat::int)) <> 0)`
|
||||
var ok bool
|
||||
if err := s.db.QueryRowContext(ctx, q, gameID, viewerID).Scan(&ok); err != nil {
|
||||
return false, fmt.Errorf("social: has unread message: %w", err)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// countUnread counts the chat entries with at least one recipient seat still unread,
|
||||
// for the chat_unread_messages gauge.
|
||||
func (s *Store) countUnread(ctx context.Context) (int64, error) {
|
||||
|
||||
Reference in New Issue
Block a user