Stage 17 round 6 (#7): reset the nudge cooldown once the player acts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
The hourly nudge cooldown now clears as soon as the sender has moved or posted a chat since their last nudge — engagement lifts the 'don't spam' limit. Backend: Nudge checks game.LastMoveAt + the sender's last non-nudge chat against the last nudge time (GameReader gains LastMoveAt). UI: nudgeOnCooldown mirrors it — a chat reset is read from the message list, a move is tracked client-side (lastActedAt on commit/pass/exchange; the backend stays authoritative across a reload). Integration test covers the reset.
This commit is contained in:
@@ -114,7 +114,15 @@ func (svc *Service) Nudge(ctx context.Context, gameID, senderID uuid.UUID) (Mess
|
||||
return Message{}, err
|
||||
}
|
||||
if ok && svc.now().Sub(last) < nudgeInterval {
|
||||
return Message{}, ErrNudgeTooSoon
|
||||
// The cooldown resets once the sender has acted (moved or chatted) since the last
|
||||
// nudge — engagement clears the "don't spam" limit (Stage 17).
|
||||
acted, err := svc.actedSince(ctx, gameID, senderID, last)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if !acted {
|
||||
return Message{}, ErrNudgeTooSoon
|
||||
}
|
||||
}
|
||||
msg, err := svc.store.insertChatMessage(ctx, gameID, senderID, kindNudge, "", nil)
|
||||
if err != nil {
|
||||
@@ -127,6 +135,22 @@ func (svc *Service) Nudge(ctx context.Context, gameID, senderID uuid.UUID) (Mess
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// actedSince reports whether senderID made a move or posted a chat message in the game
|
||||
// after t — the events that reset the nudge cooldown (Stage 17).
|
||||
func (svc *Service) actedSince(ctx context.Context, gameID, senderID uuid.UUID, t time.Time) (bool, error) {
|
||||
if mv, ok, err := svc.games.LastMoveAt(ctx, gameID, senderID); err != nil {
|
||||
return false, err
|
||||
} else if ok && mv.After(t) {
|
||||
return true, nil
|
||||
}
|
||||
if msg, ok, err := svc.store.lastMessageAt(ctx, gameID, senderID); err != nil {
|
||||
return false, err
|
||||
} else if ok && msg.After(t) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// emitChat pushes a chat message to every seated player except the sender
|
||||
// (best-effort live delivery; the recipients still read it via Messages).
|
||||
func (svc *Service) emitChat(seats []uuid.UUID, senderID uuid.UUID, m Message) {
|
||||
@@ -261,6 +285,27 @@ func (s *Store) lastNudgeAt(ctx context.Context, gameID, senderID uuid.UUID) (ti
|
||||
return row.CreatedAt, true, nil
|
||||
}
|
||||
|
||||
// lastMessageAt returns the time of senderID's most recent non-nudge chat message in
|
||||
// gameID, if any. The nudge cooldown resets when the player chats (or moves), so a stale
|
||||
// nudge no longer blocks a new one (Stage 17).
|
||||
func (s *Store) lastMessageAt(ctx context.Context, gameID, senderID uuid.UUID) (time.Time, bool, error) {
|
||||
stmt := postgres.SELECT(table.ChatMessages.CreatedAt).
|
||||
FROM(table.ChatMessages).
|
||||
WHERE(
|
||||
table.ChatMessages.GameID.EQ(postgres.UUID(gameID)).
|
||||
AND(table.ChatMessages.SenderID.EQ(postgres.UUID(senderID))).
|
||||
AND(table.ChatMessages.Kind.EQ(postgres.String(kindMessage))),
|
||||
).ORDER_BY(table.ChatMessages.CreatedAt.DESC()).LIMIT(1)
|
||||
var row model.ChatMessages
|
||||
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
|
||||
if errors.Is(err, qrm.ErrNoRows) {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
return time.Time{}, false, fmt.Errorf("social: last message: %w", err)
|
||||
}
|
||||
return row.CreatedAt, true, nil
|
||||
}
|
||||
|
||||
// messageFromRow projects a generated row into the public Message.
|
||||
func messageFromRow(r model.ChatMessages) Message {
|
||||
m := Message{
|
||||
|
||||
@@ -28,6 +28,9 @@ type GameReader interface {
|
||||
// SharedGame reports whether two accounts are seated together in any game
|
||||
// (active or finished); it gates the "befriend an opponent" request path.
|
||||
SharedGame(ctx context.Context, a, b uuid.UUID) (bool, error)
|
||||
// LastMoveAt is the time of an account's most recent move in a game (and whether it
|
||||
// has moved); the nudge cooldown resets once the player has taken a turn.
|
||||
LastMoveAt(ctx context.Context, gameID, accountID uuid.UUID) (time.Time, bool, error)
|
||||
}
|
||||
|
||||
// Sentinel errors returned by the service.
|
||||
|
||||
Reference in New Issue
Block a user