Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode -> backend REST -> existing domain services): friends (incl. one-time friend codes), per-user blocks, friend-game invitations, profile editing + email binding, the statistics screen, and the in-game history + GCG export. Friends gain two add paths (interview decision, a deliberate plan change): one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited redeem); and play-gated requests (shared game required) where an explicit decline is permanent, an ignored request lapses after 30 days, and a code bypasses a decline. Migration 00006 widens friendships_status_chk and adds friend_codes. Lobby notification badge is poll + push: a new generic `notify` event drives it live; the client polls on open/focus. Language stays a single Settings control that writes through to the durable account's preferred_language. GCG export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file. Tests: backend unit + inttest (friend gate/decline/code, ListInvitations, GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN (Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, TESTING, module READMEs.
This commit is contained in:
@@ -19,11 +19,15 @@ import (
|
||||
)
|
||||
|
||||
// GameReader is the slice of the game domain the social package needs: the seated
|
||||
// accounts in seat order, the seat index whose turn it is, and the game status.
|
||||
// game.Service satisfies it, so chat and nudge gate on game state without a
|
||||
// dependency on the engine or the game's private state.
|
||||
// accounts in seat order, the seat index whose turn it is, and the game status, plus
|
||||
// a shared-game test. game.Service satisfies it, so chat, nudge and the
|
||||
// befriend-an-opponent gate work without a dependency on the engine or the game's
|
||||
// private state.
|
||||
type GameReader interface {
|
||||
Participants(ctx context.Context, gameID uuid.UUID) (seats []uuid.UUID, toMove int, status string, err error)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Sentinel errors returned by the service.
|
||||
@@ -38,6 +42,16 @@ var (
|
||||
ErrRequestBlocked = errors.New("social: the addressee is not accepting friend requests")
|
||||
// ErrRequestNotFound is returned when no pending friend request matches.
|
||||
ErrRequestNotFound = errors.New("social: no pending friend request")
|
||||
// ErrNoSharedGame is returned when a friend request targets someone the
|
||||
// requester has never shared a game with (the befriend-an-opponent gate).
|
||||
ErrNoSharedGame = errors.New("social: you can only request someone you have played with")
|
||||
// ErrRequestDeclined is returned when the addressee has previously declined a
|
||||
// request from this requester; a re-send is refused (a one-time friend code
|
||||
// from the addressee bypasses this).
|
||||
ErrRequestDeclined = errors.New("social: this person has declined your friend request")
|
||||
// ErrFriendCodeInvalid is returned when a redeemed friend code is unknown,
|
||||
// already used, or expired.
|
||||
ErrFriendCodeInvalid = errors.New("social: friend code is invalid or expired")
|
||||
// ErrNotParticipant is returned when an account is not seated in the game.
|
||||
ErrNotParticipant = errors.New("social: account is not a player in this game")
|
||||
// ErrChatBlocked is returned when the sender has disabled chat for themselves.
|
||||
|
||||
Reference in New Issue
Block a user