Stage 17 round 6 (#16/#17, PR C): lobby sort + server-derived in-game friend state
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 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
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 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Lobby: group the my-games list into your-turn / opponent-turn / finished (empty sections hidden), ordered by last activity (your-turn oldest-first, the other two newest-first), as a compact line-separated list. gameDTO and FB GameView gain last_activity_unix (turn start while active, finish time once finished); a pure lib/lobbysort.ts holds the grouping/ordering. Friends: the in-game 'add to friends' item is now server-derived via a new GET /user/friends/outgoing (+ friends.outgoing op), returning addressees with a pending OR declined request (both read as 'request sent'), so it is correct across reloads; it shows a disabled '✓ in friends' once accepted. It live-updates when the opponent answers: RespondFriendRequest now publishes friend_added (accept) / friend_declined (new notify sub-kind, decline) to the original requester, whose open game re-derives its friend state. Tests: lobbysort unit test; gateway outgoing + last_activity transcode tests; backend integration ListOutgoingRequests + respond-publishes-to-requester; e2e updated for the new lobby section labels + a non-friend active opponent. Docs: ARCHITECTURE notify catalog, FUNCTIONAL(+ru) lobby/friends, PLAN.
This commit is contained in:
@@ -357,6 +357,7 @@ func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers
|
||||
fb.GameViewAddMoveCount(b, int32(g.MoveCount))
|
||||
fb.GameViewAddEndReason(b, endReason)
|
||||
fb.GameViewAddSeats(b, seats)
|
||||
fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix)
|
||||
return fb.GameViewEnd(b)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,16 @@ func encodeIncomingList(r backendclient.IncomingListResp) []byte {
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeOutgoingList builds an OutgoingRequestList payload.
|
||||
func encodeOutgoingList(r backendclient.OutgoingListResp) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
v := buildAccountRefVector(b, r.Requests, fb.OutgoingRequestListStartRequestsVector)
|
||||
fb.OutgoingRequestListStart(b)
|
||||
fb.OutgoingRequestListAddRequests(b, v)
|
||||
b.Finish(fb.OutgoingRequestListEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeBlockList builds a BlockList payload.
|
||||
func encodeBlockList(r backendclient.BlockListResp) []byte {
|
||||
b := flatbuffers.NewBuilder(256)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
const (
|
||||
MsgFriendsList = "friends.list"
|
||||
MsgFriendsIncoming = "friends.incoming"
|
||||
MsgFriendsOutgoing = "friends.outgoing"
|
||||
MsgFriendRequest = "friends.request"
|
||||
MsgFriendRespond = "friends.respond"
|
||||
MsgFriendCancel = "friends.cancel"
|
||||
@@ -37,6 +38,7 @@ const (
|
||||
func registerStage8(r *Registry, backend *backendclient.Client) {
|
||||
r.ops[MsgFriendsList] = Op{Handler: friendsListHandler(backend), Auth: true}
|
||||
r.ops[MsgFriendsIncoming] = Op{Handler: friendsIncomingHandler(backend), Auth: true}
|
||||
r.ops[MsgFriendsOutgoing] = Op{Handler: friendsOutgoingHandler(backend), Auth: true}
|
||||
r.ops[MsgFriendRequest] = Op{Handler: friendRequestHandler(backend), Auth: true}
|
||||
r.ops[MsgFriendRespond] = Op{Handler: friendRespondHandler(backend), Auth: true}
|
||||
r.ops[MsgFriendCancel] = Op{Handler: friendCancelHandler(backend), Auth: true}
|
||||
@@ -78,6 +80,16 @@ func friendsIncomingHandler(backend *backendclient.Client) Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func friendsOutgoingHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
res, err := backend.ListOutgoing(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeOutgoingList(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
func friendRequestHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
||||
|
||||
@@ -54,6 +54,35 @@ func TestFriendsListRoundTripDecodesNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFriendsOutgoingRoundTrip(t *testing.T) {
|
||||
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/user/friends/outgoing" {
|
||||
t.Errorf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"requests":[{"account_id":"o-1","display_name":"Pat"}]}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
reg := transcode.NewRegistry(backend, nil)
|
||||
op, ok := reg.Lookup(transcode.MsgFriendsOutgoing)
|
||||
if !ok {
|
||||
t.Fatal("friends.outgoing not registered")
|
||||
}
|
||||
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("handler: %v", err)
|
||||
}
|
||||
ol := fb.GetRootAsOutgoingRequestList(payload, 0)
|
||||
if ol.RequestsLength() != 1 {
|
||||
t.Fatalf("outgoing length = %d, want 1", ol.RequestsLength())
|
||||
}
|
||||
var ref fb.AccountRef
|
||||
ol.Requests(&ref, 0)
|
||||
if string(ref.AccountId()) != "o-1" || string(ref.DisplayName()) != "Pat" {
|
||||
t.Fatalf("outgoing[0] = (%q, %q), want (o-1, Pat)", ref.AccountId(), ref.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFriendRequestForwardsTarget(t *testing.T) {
|
||||
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-User-ID"); got != "u-1" {
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestGamesListRoundTripDecodesSeatNames(t *testing.T) {
|
||||
if r.URL.Path != "/api/v1/user/games" {
|
||||
t.Errorf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"games":[{"id":"g-1","variant":"english","status":"active","players":2,"to_move":0,"seats":[{"seat":0,"account_id":"u-9","display_name":"You","score":10},{"seat":1,"account_id":"a-1","display_name":"Ann","score":7}]}]}`))
|
||||
_, _ = w.Write([]byte(`{"games":[{"id":"g-1","variant":"english","status":"active","players":2,"to_move":0,"last_activity_unix":1717000000,"seats":[{"seat":0,"account_id":"u-9","display_name":"You","score":10},{"seat":1,"account_id":"a-1","display_name":"Ann","score":7}]}]}`))
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
@@ -177,6 +177,9 @@ func TestGamesListRoundTripDecodesSeatNames(t *testing.T) {
|
||||
if string(g.Id()) != "g-1" {
|
||||
t.Errorf("game id = %q, want g-1", g.Id())
|
||||
}
|
||||
if g.LastActivityUnix() != 1717000000 {
|
||||
t.Errorf("last activity = %d, want 1717000000", g.LastActivityUnix())
|
||||
}
|
||||
var seat fb.SeatView
|
||||
g.Seats(&seat, 1)
|
||||
if string(seat.DisplayName()) != "Ann" {
|
||||
|
||||
Reference in New Issue
Block a user