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

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:
Ilia Denisov
2026-06-08 19:23:48 +02:00
parent b720907db2
commit 6b6baf5710
40 changed files with 743 additions and 81 deletions
+11 -2
View File
@@ -90,6 +90,7 @@ export class MockGateway implements GatewayClient {
private pendingMatch: string | null = null;
private friends: AccountRef[] = MOCK_FRIENDS.map((f) => ({ ...f }));
private incoming: AccountRef[] = MOCK_INCOMING.map((f) => ({ ...f }));
private outgoing: AccountRef[] = [];
private blocks: AccountRef[] = [];
private invitations: Invitation[] = mockInvitations();
private readonly stats: Stats = { ...MOCK_STATS };
@@ -155,6 +156,7 @@ export class MockGateway implements GatewayClient {
turnTimeoutSecs: 86400,
moveCount: 0,
endReason: '',
lastActivityUnix: Math.floor(Date.now() / 1000),
seats: [
{ seat: 0, accountId: ME, displayName: 'You', score: 0, hintsUsed: 0, isWinner: false },
{ seat: 1, accountId: 'robot', displayName: 'Robo', score: 0, hintsUsed: 0, isWinner: false },
@@ -372,8 +374,15 @@ export class MockGateway implements GatewayClient {
async friendsIncoming(): Promise<AccountRef[]> {
return this.incoming.map((f) => ({ ...f }));
}
async friendRequest(_accountId: string): Promise<void> {
// The real backend requires a shared game; the mock simply acknowledges.
async friendsOutgoing(): Promise<AccountRef[]> {
return this.outgoing.map((f) => ({ ...f }));
}
async friendRequest(accountId: string): Promise<void> {
// The real backend requires a shared game; the mock records the outgoing request so
// the game's "add to friends" item reads as sent across reloads.
if (!this.outgoing.some((o) => o.accountId === accountId)) {
this.outgoing.push({ accountId, displayName: this.nameFor(accountId) });
}
}
async friendRespond(requesterId: string, accept: boolean): Promise<void> {
const i = this.incoming.findIndex((r) => r.accountId === requesterId);