feat(avatars): show the uploaded profile photo everywhere
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:
@@ -8,7 +8,7 @@ namespace Hokm.Server.Game;
|
||||
public record CardDto(string Suit, int Rank, string Id);
|
||||
public record PlayedCardDto(int Seat, CardDto Card);
|
||||
public record PlayerDto(int Seat, string Name, int Team, bool IsHuman, int HandCount, List<CardDto>? Hand);
|
||||
public record SeatPlayerDto(int Seat, string Name, string Avatar, int Level, bool Connected, bool IsBot, string? UserId);
|
||||
public record SeatPlayerDto(int Seat, string Name, string Avatar, int Level, bool Connected, bool IsBot, string? UserId, string? AvatarImage = null);
|
||||
public record RoundResultDto(int WinningTeam, int[] Tricks, bool Kot, int Points);
|
||||
|
||||
public record GameStateDto(
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace Hokm.Server.Game;
|
||||
|
||||
// Wire DTOs for private rooms (camelCase JSON → TS client types).
|
||||
public record RoomPlayerDto(string Id, string DisplayName, string Avatar, int Level);
|
||||
public record RoomPlayerDto(string Id, string DisplayName, string Avatar, int Level, string? AvatarImage = null);
|
||||
public record RoomSeatDto(int Seat, string Kind, RoomPlayerDto? Player); // kind: empty|invited|bot|human
|
||||
public record RoomDto(string Id, string Code, string HostId, string Status, List<RoomSeatDto> Seats, int TargetScore, int Stake, bool Ranked);
|
||||
public record RoomInviteDto(string RoomId, string Code, string HostName, int Stake);
|
||||
@@ -26,6 +26,7 @@ public sealed partial class GameManager
|
||||
public string? UserId;
|
||||
public string Name = "";
|
||||
public string Avatar = "a-fox";
|
||||
public string? AvatarImage;
|
||||
public int Level;
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ public sealed partial class GameManager
|
||||
{
|
||||
var room = new PRoom { HostId = host.UserId, Stake = stake, TargetScore = target <= 0 ? 7 : target };
|
||||
for (int i = 0; i < 4; i++) room.Seats[i] = new PSeat { Seat = i };
|
||||
room.Seats[0] = new PSeat { Seat = 0, Kind = "human", UserId = host.UserId, Name = host.Name, Avatar = host.Avatar, Level = host.Level };
|
||||
room.Seats[0] = new PSeat { Seat = 0, Kind = "human", UserId = host.UserId, Name = host.Name, Avatar = host.Avatar, AvatarImage = host.AvatarImage, Level = host.Level };
|
||||
_privateRooms[room.Id] = room;
|
||||
_userPrivate[host.UserId] = room.Id;
|
||||
PushRoom(room);
|
||||
@@ -62,14 +63,14 @@ public sealed partial class GameManager
|
||||
{
|
||||
if (seat is < 1 or > 3 || string.IsNullOrEmpty(friendId) || friendId == hostId) return;
|
||||
// Authoritative friend identity (so the pending seat shows their real name/avatar).
|
||||
var (name, avatar, level) = ResolveProfile(friendId, "", "a-fox", 1);
|
||||
var (name, avatar, level, avatarImage) = ResolveProfile(friendId, "", "a-fox", 1);
|
||||
lock (_proomLock)
|
||||
{
|
||||
if (!HostRoom(hostId, out var room)) return;
|
||||
if (room!.Seats[seat].Kind is not ("empty" or "invited")) return;
|
||||
if (room.Seats.Any(s => s.Seat != seat && s.UserId == friendId)) return; // already in this room
|
||||
FreeSeatInvite(room.Seats[seat]); // if re-inviting over a prior invite
|
||||
room.Seats[seat] = new PSeat { Seat = seat, Kind = "invited", UserId = friendId, Name = name, Avatar = avatar, Level = level };
|
||||
room.Seats[seat] = new PSeat { Seat = seat, Kind = "invited", UserId = friendId, Name = name, Avatar = avatar, AvatarImage = avatarImage, Level = level };
|
||||
_pendingInvite[friendId] = room.Id;
|
||||
_ = _hub.Clients.User(friendId).SendAsync("roomInvite", new RoomInviteDto(room.Id, room.Code, room.Seats[0].Name, room.Stake));
|
||||
PushRoom(room);
|
||||
@@ -83,8 +84,8 @@ public sealed partial class GameManager
|
||||
if (!_pendingInvite.TryRemove(userId, out var roomId) || !_privateRooms.TryGetValue(roomId, out var room)) return;
|
||||
var seat = room.Seats.FirstOrDefault(x => x.Kind == "invited" && x.UserId == userId);
|
||||
if (seat == null) return;
|
||||
var (name, avatar, level) = ResolveProfile(userId, seat.Name, seat.Avatar, seat.Level);
|
||||
room.Seats[seat.Seat] = new PSeat { Seat = seat.Seat, Kind = "human", UserId = userId, Name = name, Avatar = avatar, Level = level };
|
||||
var (name, avatar, level, avatarImage) = ResolveProfile(userId, seat.Name, seat.Avatar, seat.Level);
|
||||
room.Seats[seat.Seat] = new PSeat { Seat = seat.Seat, Kind = "human", UserId = userId, Name = name, Avatar = avatar, AvatarImage = avatarImage, Level = level };
|
||||
_userPrivate[userId] = room.Id;
|
||||
PushRoom(room);
|
||||
}
|
||||
@@ -139,7 +140,7 @@ public sealed partial class GameManager
|
||||
{
|
||||
var s = room.Seats[i];
|
||||
slots[i] = s.Kind == "human"
|
||||
? new SeatSlot { Seat = i, UserId = s.UserId, Name = s.Name, Avatar = s.Avatar, Level = s.Level }
|
||||
? new SeatSlot { Seat = i, UserId = s.UserId, Name = s.Name, Avatar = s.Avatar, AvatarImage = s.AvatarImage, Level = s.Level }
|
||||
: new SeatSlot { Seat = i, IsBot = true,
|
||||
Name = s.Kind == "bot" && s.Name.Length > 0 ? s.Name : BotNames[_rng.Next(BotNames.Length)],
|
||||
Avatar = s.Kind == "bot" ? s.Avatar : Avatars[_rng.Next(Avatars.Length)],
|
||||
@@ -198,16 +199,16 @@ public sealed partial class GameManager
|
||||
}
|
||||
}
|
||||
|
||||
private (string name, string avatar, int level) ResolveProfile(string userId, string fbName, string fbAvatar, int fbLevel)
|
||||
private (string name, string avatar, int level, string? avatarImage) ResolveProfile(string userId, string fbName, string fbAvatar, int fbLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _scopes.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<ProfileService>();
|
||||
var p = svc.GetOrCreate(userId, null).GetAwaiter().GetResult();
|
||||
return (p.DisplayName, p.Avatar, p.Level);
|
||||
return (p.DisplayName, p.Avatar, p.Level, p.AvatarImage);
|
||||
}
|
||||
catch { return (fbName, fbAvatar, fbLevel); }
|
||||
catch { return (fbName, fbAvatar, fbLevel, null); }
|
||||
}
|
||||
|
||||
private void PushRoom(PRoom room)
|
||||
@@ -225,7 +226,7 @@ public sealed partial class GameManager
|
||||
private static RoomSeatDto SeatDto(PSeat s) =>
|
||||
s.Kind == "empty"
|
||||
? new RoomSeatDto(s.Seat, "empty", null)
|
||||
: new RoomSeatDto(s.Seat, s.Kind, new RoomPlayerDto(s.UserId ?? $"bot-{s.Seat}", s.Name, s.Avatar, s.Level));
|
||||
: new RoomSeatDto(s.Seat, s.Kind, new RoomPlayerDto(s.UserId ?? $"bot-{s.Seat}", s.Name, s.Avatar, s.Level, s.Kind == "bot" ? null : s.AvatarImage));
|
||||
|
||||
/// <summary>Turn a fixed seat arrangement into a live match (used by private-room start).</summary>
|
||||
private void StartMatchSeats(SeatSlot[] seats, int stake, int targetScore)
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class Player
|
||||
public required string UserId { get; init; }
|
||||
public string Name { get; init; } = "";
|
||||
public string Avatar { get; init; } = "a-fox";
|
||||
public string? AvatarImage { get; init; }
|
||||
public int Level { get; init; }
|
||||
public string Plan { get; init; } = "free";
|
||||
}
|
||||
@@ -142,7 +143,7 @@ public sealed partial class GameManager
|
||||
if (i < humans.Count)
|
||||
{
|
||||
var h = humans[i];
|
||||
seats[i] = new SeatSlot { Seat = i, UserId = h.UserId, Name = h.Name, Avatar = h.Avatar, Level = h.Level };
|
||||
seats[i] = new SeatSlot { Seat = i, UserId = h.UserId, Name = h.Name, Avatar = h.Avatar, AvatarImage = h.AvatarImage, Level = h.Level };
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class SeatSlot
|
||||
public string? UserId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Avatar { get; set; } = "a-fox";
|
||||
public string? AvatarImage { get; set; }
|
||||
public int Level { get; set; }
|
||||
public bool IsBot { get; set; }
|
||||
public bool Connected { get; set; } = true;
|
||||
@@ -413,7 +414,7 @@ public sealed class GameRoom : IDisposable
|
||||
p.Seat == viewerSeat ? p.Hand.Select(Map.Card).ToList() : null)).ToList();
|
||||
|
||||
var seatPlayers = Seats.OrderBy(s => s.Seat)
|
||||
.Select(s => new SeatPlayerDto(s.Seat, s.Name, s.Avatar, s.Level, s.Connected, s.IsBot, s.IsBot ? null : s.UserId)).ToList();
|
||||
.Select(s => new SeatPlayerDto(s.Seat, s.Name, s.Avatar, s.Level, s.Connected, s.IsBot, s.IsBot ? null : s.UserId, s.IsBot ? null : s.AvatarImage)).ToList();
|
||||
|
||||
RoundResultDto? rr = State.LastRoundResult is null ? null
|
||||
: new RoundResultDto(State.LastRoundResult.WinningTeam, State.LastRoundResult.Tricks,
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Hokm.Server.Hubs;
|
||||
|
||||
public record MatchmakeRequest(string Name, string Avatar, int Level, string Plan);
|
||||
public record MatchmakeRequest(string Name, string Avatar, int Level, string Plan, string? AvatarImage = null);
|
||||
|
||||
[Authorize]
|
||||
public sealed class GameHub : Hub
|
||||
@@ -32,6 +32,7 @@ public sealed class GameHub : Hub
|
||||
UserId = Uid,
|
||||
Name = string.IsNullOrWhiteSpace(req.Name) ? "بازیکن" : req.Name,
|
||||
Avatar = req.Avatar,
|
||||
AvatarImage = req.AvatarImage,
|
||||
Level = req.Level,
|
||||
Plan = req.Plan,
|
||||
});
|
||||
@@ -65,6 +66,7 @@ public sealed class GameHub : Hub
|
||||
UserId = Uid,
|
||||
Name = string.IsNullOrWhiteSpace(req.Name) ? "بازیکن" : req.Name,
|
||||
Avatar = req.Avatar,
|
||||
AvatarImage = req.AvatarImage,
|
||||
Level = req.Level,
|
||||
Plan = req.Plan,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ public class FriendDto
|
||||
public string Username { get; set; } = "";
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string Avatar { get; set; } = "a-fox";
|
||||
public string? AvatarImage { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Rating { get; set; }
|
||||
public string Status { get; set; } = "offline"; // online | offline
|
||||
|
||||
@@ -60,6 +60,7 @@ public class SocialService
|
||||
Username = p?.Username ?? userId,
|
||||
DisplayName = p?.DisplayName ?? userId,
|
||||
Avatar = p?.Avatar ?? "a-fox",
|
||||
AvatarImage = p?.AvatarImage,
|
||||
Level = p?.Level ?? 1,
|
||||
Rating = p?.Rating ?? 1000,
|
||||
Status = _mgr.IsOnline(userId) ? "online" : "offline",
|
||||
|
||||
Reference in New Issue
Block a user