feat(avatars): show the uploaded profile photo everywhere
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m35s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m17s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m12s

Previously the uploaded profile photo only appeared in a few places (profile,
top bar, leaderboard, public profile); chat, friends, game table, match intro,
post-match roster and private rooms showed the emoji avatar only.

- carry avatarImage end-to-end:
  - server DTOs: FriendDto, SeatPlayerDto, RoomPlayerDto, MatchmakeRequest +
    Player/SeatSlot/PSeat; ResolveProfile now returns avatarImage; FriendDtoFor
    fills it from the profile.
  - client types: Friend, RoomSeat.player, MatchmakingState.players,
    ServerSeatPlayer, SeatPlayer (adds avatarId + avatarImage).
  - signalr-service: send my avatarImage on StartMatchmaking/CreatePrivateRoom;
    carry it through mapRoom.
  - game-store: applyServerState + newOnlineMatch + offline match now populate
    avatarId/avatarImage (seat 0 uses your own profile photo).
- render every avatar through the shared <Avatar> component (image → emoji
  fallback): ChatScreen, FriendsScreen (requests/friends/chats), GameTable
  seats, MatchIntroOverlay, MatchPlayersList, RoomScreen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-17 08:17:27 +03:30
parent e5b48ecb26
commit 4739018488
16 changed files with 85 additions and 47 deletions
+29 -15
View File
@@ -39,7 +39,9 @@ export type GameMode = "ai" | "online";
export interface SeatPlayer {
name: string;
avatar: string; // emoji
avatar: string; // emoji (legacy/fallback)
avatarId?: string; // avatar id (for the shared <Avatar> component)
avatarImage?: string | null; // uploaded profile photo — shown everywhere when present
level: number;
id?: string; // real player's user id (for add-friend); absent for bots/you
isBot?: boolean;
@@ -54,7 +56,7 @@ export interface GameSettings {
}
export interface OnlineMatchConfig {
players: { displayName: string; avatar: string; level: number }[]; // index = seat
players: { displayName: string; avatar: string; level: number; avatarImage?: string | null }[]; // index = seat
targetScore: number;
stake: number;
ranked: boolean;
@@ -338,13 +340,18 @@ export const useGameStore = create<GameStore>((set, get) => {
turnDeadline: null,
disconnectedSeat: null,
reconnectDeadline: null,
seatPlayers: settings.names.map((name, i) => ({
name,
avatar: AI_AVATARS[i],
level: 0,
isBot: i > 0, // seat 0 is you
title: i === 0 ? useSessionStore.getState().profile?.title ?? null : null,
})),
seatPlayers: settings.names.map((name, i) => {
const prof = i === 0 ? useSessionStore.getState().profile : null;
return {
name,
avatar: AI_AVATARS[i],
avatarId: prof?.avatar, // you → your avatar; bots fall back to the emoji
avatarImage: prof?.avatarImage ?? null,
level: 0,
isBot: i > 0, // seat 0 is you
title: i === 0 ? prof?.title ?? null : null,
};
}),
});
scheduleAuto();
},
@@ -368,12 +375,17 @@ export const useGameStore = create<GameStore>((set, get) => {
turnDeadline: null,
disconnectedSeat: null,
reconnectDeadline: null,
seatPlayers: cfg.players.map((p, i) => ({
name: p.displayName,
avatar: avatarEmoji(p.avatar),
level: p.level,
title: i === 0 ? useSessionStore.getState().profile?.title ?? null : null,
})),
seatPlayers: cfg.players.map((p, i) => {
const prof = i === 0 ? useSessionStore.getState().profile : null;
return {
name: p.displayName,
avatar: avatarEmoji(p.avatar),
avatarId: p.avatar,
avatarImage: p.avatarImage ?? prof?.avatarImage ?? null,
level: p.level,
title: i === 0 ? prof?.title ?? null : null,
};
}),
});
scheduleAuto();
},
@@ -416,6 +428,8 @@ export const useGameStore = create<GameStore>((set, get) => {
.map((sp) => ({
name: sp.name,
avatar: avatarEmoji(sp.avatar),
avatarId: sp.avatar,
avatarImage: sp.avatarImage ?? null,
level: sp.level,
id: sp.userId,
isBot: sp.isBot,