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

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:
Ilia Denisov
2026-06-19 16:50:48 +02:00
parent c67a5d51f1
commit 6e77de4c1e
34 changed files with 331 additions and 86 deletions
+4
View File
@@ -76,6 +76,10 @@ table GameView {
// unread_chat is a per-viewer flag: the requesting player has at least one unread chat
// entry (message or nudge) in this game. It drives the lobby and in-game unread badge.
unread_chat:bool;
// unread_messages is a per-viewer flag: at least one of the unread entries is a real chat
// message (not just a nudge). With unread_chat it colours the badge — both set is the regular
// badge, unread_chat alone is the softer nudge-only badge.
unread_messages:bool;
}
// MoveRecord is one decoded move (a committed play, or a hint preview).
+16 -1
View File
@@ -197,8 +197,20 @@ func (rcv *GameView) MutateUnreadChat(n bool) bool {
return rcv._tab.MutateBoolSlot(30, n)
}
func (rcv *GameView) UnreadMessages() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *GameView) MutateUnreadMessages(n bool) bool {
return rcv._tab.MutateBoolSlot(32, n)
}
func GameViewStart(builder *flatbuffers.Builder) {
builder.StartObject(14)
builder.StartObject(15)
}
func GameViewAddId(builder *flatbuffers.Builder, id flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(id), 0)
@@ -245,6 +257,9 @@ func GameViewAddVsAi(builder *flatbuffers.Builder, vsAi bool) {
func GameViewAddUnreadChat(builder *flatbuffers.Builder, unreadChat bool) {
builder.PrependBoolSlot(13, unreadChat, false)
}
func GameViewAddUnreadMessages(builder *flatbuffers.Builder, unreadMessages bool) {
builder.PrependBoolSlot(14, unreadMessages, false)
}
func GameViewEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+4
View File
@@ -46,6 +46,9 @@ type GameView struct {
VsAI bool
// UnreadChat is a per-viewer flag: the requesting player has unread chat in this game.
UnreadChat bool
// UnreadMessages is a per-viewer flag: at least one unread entry is a real message (not just
// a nudge), so the badge can be coloured apart from the nudge-only case.
UnreadMessages bool
}
// TileRecord is one tile in a decoded MoveRecord (the concrete letter, "?" for a blank
@@ -165,6 +168,7 @@ func BuildGameView(b *flatbuffers.Builder, g GameView) flatbuffers.UOffsetT {
fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix)
fb.GameViewAddVsAi(b, g.VsAI)
fb.GameViewAddUnreadChat(b, g.UnreadChat)
fb.GameViewAddUnreadMessages(b, g.UnreadMessages)
return fb.GameViewEnd(b)
}