feat(chess): avatar vs avatar display on board and game list
Add ChessPlayerChip.kt with three shared composables: - ChessPlayerChip: single player avatar + name with active border - ChessPlayerVsHeader: mirrored White vs Black header above the board - OverlappingAvatars: compact overlapping circular avatars for list cards Wire avatars into: - LiveChessGameScreen: vs header with active player green border - DesktopChessGameLayout: vs header above the chess board - Android & Desktop lobby cards: overlapping avatars in ActiveGameCard, ChallengeCard, and OutgoingChallengeCard avatar slots Uses existing UserAvatar (coil3 AsyncImage + Robohash fallback) from commons for KMP compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+39
-3
@@ -295,12 +295,42 @@ fun ChessGameScreen(
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Resolve opponent display name
|
||||
// Resolve opponent display name and avatar
|
||||
val opponentUser =
|
||||
remember(gameState.opponentPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(gameState.opponentPubkey)
|
||||
}
|
||||
val opponentDisplayName =
|
||||
remember(gameState.opponentPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(gameState.opponentPubkey)?.toBestDisplayName()
|
||||
?: gameState.opponentPubkey.take(8)
|
||||
opponentUser?.toBestDisplayName() ?: gameState.opponentPubkey.take(8)
|
||||
}
|
||||
val opponentAvatarUrl =
|
||||
remember(gameState.opponentPubkey) {
|
||||
opponentUser?.profilePicture()
|
||||
}
|
||||
|
||||
// Resolve player display name and avatar
|
||||
val playerUser =
|
||||
remember(gameState.playerPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(gameState.playerPubkey)
|
||||
}
|
||||
val playerDisplayName =
|
||||
remember(gameState.playerPubkey) {
|
||||
playerUser?.toBestDisplayName() ?: gameState.playerPubkey.take(8)
|
||||
}
|
||||
val playerAvatarUrl =
|
||||
remember(gameState.playerPubkey) {
|
||||
playerUser?.profilePicture()
|
||||
}
|
||||
|
||||
// Determine white/black pubkeys, names and avatars based on player color
|
||||
val isPlayerWhite = gameState.playerColor == com.vitorpamplona.quartz.nip64Chess.Color.WHITE
|
||||
val whitePubkey = if (isPlayerWhite) gameState.playerPubkey else gameState.opponentPubkey
|
||||
val blackPubkey = if (isPlayerWhite) gameState.opponentPubkey else gameState.playerPubkey
|
||||
val whiteDisplayName = if (isPlayerWhite) playerDisplayName else opponentDisplayName
|
||||
val blackDisplayName = if (isPlayerWhite) opponentDisplayName else playerDisplayName
|
||||
val whiteAvatarUrl = if (isPlayerWhite) playerAvatarUrl else opponentAvatarUrl
|
||||
val blackAvatarUrl = if (isPlayerWhite) opponentAvatarUrl else playerAvatarUrl
|
||||
|
||||
// Determine spectator status:
|
||||
// 1. If game was accepted locally, user is definitely NOT a spectator
|
||||
@@ -320,6 +350,12 @@ fun ChessGameScreen(
|
||||
onResign = { chessViewModel.resign(gameId) },
|
||||
isSpectatorOverride = isSpectating,
|
||||
onGameEndDismiss = { chessViewModel.dismissGame(gameId) },
|
||||
whiteName = whiteDisplayName,
|
||||
whiteHex = whitePubkey,
|
||||
whiteAvatarUrl = whiteAvatarUrl,
|
||||
blackName = blackDisplayName,
|
||||
blackHex = blackPubkey,
|
||||
blackAvatarUrl = blackAvatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+60
-4
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.chess.ChessChallenge
|
||||
import com.vitorpamplona.amethyst.commons.chess.ChessSyncBanner
|
||||
import com.vitorpamplona.amethyst.commons.chess.NewChessGameDialog
|
||||
import com.vitorpamplona.amethyst.commons.chess.OutgoingChallengeCard
|
||||
import com.vitorpamplona.amethyst.commons.chess.OverlappingAvatars
|
||||
import com.vitorpamplona.amethyst.commons.chess.PublicGameCard
|
||||
import com.vitorpamplona.amethyst.commons.chess.SpectatingGameCard
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
@@ -359,15 +360,28 @@ fun ChessLobbyContent(
|
||||
}
|
||||
|
||||
items(activeGames.entries.toList(), key = { "active-${it.key}" }) { (gameId, state) ->
|
||||
val displayName =
|
||||
val opponent =
|
||||
remember(state.opponentPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(state.opponentPubkey)?.toBestDisplayName() ?: state.opponentPubkey.take(8)
|
||||
accountViewModel.checkGetOrCreateUser(state.opponentPubkey)
|
||||
}
|
||||
val displayName = opponent?.toBestDisplayName() ?: state.opponentPubkey.take(8)
|
||||
val playerUser =
|
||||
remember(state.playerPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(state.playerPubkey)
|
||||
}
|
||||
ActiveGameCard(
|
||||
gameId = gameId,
|
||||
opponentName = displayName,
|
||||
isYourTurn = state.isPlayerTurn(),
|
||||
onClick = { onSelectGame(gameId) },
|
||||
avatar = {
|
||||
OverlappingAvatars(
|
||||
avatar1Hex = state.playerPubkey,
|
||||
avatar2Hex = state.opponentPubkey,
|
||||
avatar1Url = playerUser?.profilePicture(),
|
||||
avatar2Url = opponent?.profilePicture(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -386,14 +400,32 @@ fun ChessLobbyContent(
|
||||
}
|
||||
|
||||
items(outgoingChallenges, key = { "outgoing-${it.eventId}" }) { challenge ->
|
||||
val opponentUser =
|
||||
remember(challenge.opponentPubkey) {
|
||||
challenge.opponentPubkey?.let { accountViewModel.checkGetOrCreateUser(it) }
|
||||
}
|
||||
val opponentName =
|
||||
challenge.opponentPubkey?.let { pubkey ->
|
||||
accountViewModel.checkGetOrCreateUser(pubkey)?.toBestDisplayName() ?: pubkey.take(8)
|
||||
opponentUser?.toBestDisplayName()
|
||||
?: challenge.opponentPubkey?.take(8)
|
||||
val currentUser =
|
||||
remember(userPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(userPubkey)
|
||||
}
|
||||
OutgoingChallengeCard(
|
||||
opponentName = opponentName,
|
||||
userPlaysWhite = challenge.challengerColor == ChessColor.WHITE,
|
||||
onClick = { onOpenOwnChallenge(challenge) },
|
||||
avatar =
|
||||
challenge.opponentPubkey?.let { opPubkey ->
|
||||
{
|
||||
OverlappingAvatars(
|
||||
avatar1Hex = userPubkey,
|
||||
avatar2Hex = opPubkey,
|
||||
avatar1Url = currentUser?.profilePicture(),
|
||||
avatar2Url = opponentUser?.profilePicture(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -442,11 +474,23 @@ fun ChessLobbyContent(
|
||||
?: accountViewModel.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName()
|
||||
?: challenge.challengerPubkey.take(8)
|
||||
}
|
||||
val currentUser =
|
||||
remember(userPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(userPubkey)
|
||||
}
|
||||
ChallengeCard(
|
||||
challengerName = displayName,
|
||||
challengerPlaysWhite = challenge.challengerColor == ChessColor.WHITE,
|
||||
isIncoming = true,
|
||||
onAccept = { onAcceptChallenge(challenge) },
|
||||
avatar = {
|
||||
OverlappingAvatars(
|
||||
avatar1Hex = challenge.challengerPubkey,
|
||||
avatar2Hex = userPubkey,
|
||||
avatar1Url = challenge.challengerAvatarUrl,
|
||||
avatar2Url = currentUser?.profilePicture(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -471,11 +515,23 @@ fun ChessLobbyContent(
|
||||
?: accountViewModel.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName()
|
||||
?: challenge.challengerPubkey.take(8)
|
||||
}
|
||||
val currentUser =
|
||||
remember(userPubkey) {
|
||||
accountViewModel.checkGetOrCreateUser(userPubkey)
|
||||
}
|
||||
ChallengeCard(
|
||||
challengerName = displayName,
|
||||
challengerPlaysWhite = challenge.challengerColor == ChessColor.WHITE,
|
||||
isIncoming = false,
|
||||
onAccept = { onAcceptChallenge(challenge) },
|
||||
avatar = {
|
||||
OverlappingAvatars(
|
||||
avatar1Hex = challenge.challengerPubkey,
|
||||
avatar2Hex = userPubkey,
|
||||
avatar1Url = challenge.challengerAvatarUrl,
|
||||
avatar2Url = currentUser?.profilePicture(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user