fix(chess): filter own games from live list, dismissable errors, clickable avatars and completed games

- Filter user's own games from public "Live Games" list in lobby
- Fix broken addSpectatingGame filter (playerPubkey is always viewerPubkey)
- Add removeGame() to clean up stale entries when "Game not found"
- Make player avatars clickable to open profile on game screen
- Make completed game cards clickable to view final board state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
davotoula
2026-03-23 18:01:29 +01:00
parent 08d5f2ebb5
commit c86daa6950
10 changed files with 47 additions and 4 deletions
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.chess.ChessBroadcastStatus
import com.vitorpamplona.amethyst.commons.chess.ChessSyncBanner
import com.vitorpamplona.amethyst.commons.chess.LiveChessGameScreen
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
import com.vitorpamplona.amethyst.ui.stringRes
@@ -283,7 +284,10 @@ fun ChessGameScreen(
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = { nav.popBack() }) {
Button(onClick = {
chessViewModel.removeGame(gameId)
nav.popBack()
}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringRes(R.string.back),
@@ -365,6 +369,7 @@ fun ChessGameScreen(
blackName = blackDisplayName,
blackHex = blackPubkey,
blackAvatarUrl = blackAvatarUrl,
onPlayerClick = { pubkeyHex -> nav.nav(Route.Profile(pubkeyHex)) },
)
}
}
@@ -596,6 +596,7 @@ fun ChessLobbyContent(
didUserWin = game.didUserWin(userPubkey),
isDraw = game.isDraw,
moveCount = game.moveCount,
onClick = { onSelectGame(game.gameId) },
avatar = {
OverlappingAvatars(
avatar1Hex = userPubkey,
@@ -178,6 +178,8 @@ class ChessViewModelNew(
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
fun removeGame(gameId: String) = logic.removeGame(gameId)
// ============================================
// Utility
// ============================================
@@ -288,6 +288,7 @@ fun CompletedGameCard(
didUserWin: Boolean,
isDraw: Boolean,
moveCount: Int,
onClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
avatar: @Composable (() -> Unit)? = null,
) {
@@ -309,6 +310,8 @@ fun CompletedGameCard(
}
Card(
onClick = onClick ?: {},
enabled = onClick != null,
modifier = modifier.fillMaxWidth(),
) {
Row(
@@ -856,6 +856,16 @@ class ChessLobbyLogic(
}
}
/**
* Remove a game from all lobby lists (active, spectating) and stop polling.
* Used when a game fails to load ("Game not found") so the user can dismiss the stale entry.
*/
fun removeGame(gameId: String) {
state.removeActiveGame(gameId)
state.removeSpectatingGame(gameId)
pollingDelegate.removeGameId(gameId)
}
fun clearError() {
state.setError(null)
}
@@ -297,7 +297,8 @@ class ChessLobbyState(
}
fun updatePublicGames(games: List<PublicGame>) {
_publicGames.value = games
// Filter out games where the user is a participant
_publicGames.value = games.filter { it.whitePubkey != userPubkey && it.blackPubkey != userPubkey }
}
fun addActiveGame(
@@ -501,7 +502,9 @@ class ChessLobbyState(
return
}
// Don't add own games to spectating list
if (state.playerPubkey == userPubkey || state.opponentPubkey == userPubkey) {
// Note: LiveChessGameState.playerPubkey is always viewerPubkey regardless of role,
// so we check isSpectator instead to determine if the user is actually a participant
if (!state.isSpectator) {
return
}
// Don't add finished games to spectating list
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.chess
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -64,6 +65,7 @@ fun ChessPlayerChip(
avatarUrl: String?,
isActive: Boolean = false,
mirrored: Boolean = false,
onClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val avatar =
@@ -102,7 +104,10 @@ fun ChessPlayerChip(
}
Row(
modifier = modifier.alpha(if (isActive) 1f else 0.6f),
modifier =
modifier
.alpha(if (isActive) 1f else 0.6f)
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
@@ -146,6 +151,8 @@ fun ChessPlayerVsHeader(
blackHex: String,
blackAvatarUrl: String?,
isWhiteTurn: Boolean,
onWhiteClick: (() -> Unit)? = null,
onBlackClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
Row(
@@ -163,6 +170,7 @@ fun ChessPlayerVsHeader(
userHex = whiteHex,
avatarUrl = whiteAvatarUrl,
isActive = isWhiteTurn,
onClick = onWhiteClick,
)
Text(
text = "White",
@@ -188,6 +196,7 @@ fun ChessPlayerVsHeader(
avatarUrl = blackAvatarUrl,
isActive = !isWhiteTurn,
mirrored = true,
onClick = onBlackClick,
)
Text(
text = "Black",
@@ -208,6 +208,7 @@ fun LiveChessGameScreen(
blackName: String = "Black",
blackHex: String = "",
blackAvatarUrl: String? = null,
onPlayerClick: ((pubkeyHex: String) -> Unit)? = null,
) {
// Observe state flows for automatic recomposition on updates
val currentPosition by gameState.currentPosition.collectAsState()
@@ -253,6 +254,8 @@ fun LiveChessGameScreen(
blackHex = blackHex,
blackAvatarUrl = blackAvatarUrl,
isWhiteTurn = currentPosition.activeColor == Color.WHITE,
onWhiteClick = onPlayerClick?.let { { it(whiteHex) } },
onBlackClick = onPlayerClick?.let { { it(blackHex) } },
modifier = Modifier.fillMaxWidth(),
)
}
@@ -252,8 +252,12 @@ fun ChessScreen(
// Main content
if (selectedGameId != null) {
// Set focused game mode - only poll this game, not others
// If game isn't in active/spectating maps (e.g. completed game), load from relays
LaunchedEffect(selectedGameId) {
viewModel.setFocusedGame(selectedGameId!!)
if (viewModel.getGameState(selectedGameId!!) == null) {
viewModel.loadGame(selectedGameId!!)
}
}
// Use stateVersion to ensure recomposition when game state changes
@@ -591,6 +595,7 @@ private fun ChessLobby(
didUserWin = game.didUserWin(userPubkey),
isDraw = game.isDraw,
moveCount = game.moveCount,
onClick = { onSelectGame(game.gameId) },
avatar = {
UserAvatar(
userHex = opponentPubkey,
@@ -174,6 +174,8 @@ class DesktopChessViewModelNew(
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
fun removeGame(gameId: String) = logic.removeGame(gameId)
// ============================================
// Utility
// ============================================