diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index 702fc5fab..46231e950 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -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)) }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index 333d9c6e1..5a8f0b416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -596,6 +596,7 @@ fun ChessLobbyContent( didUserWin = game.didUserWin(userPubkey), isDraw = game.isDraw, moveCount = game.moveCount, + onClick = { onSelectGame(game.gameId) }, avatar = { OverlappingAvatars( avatar1Hex = userPubkey, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt index ef1466285..97711b9e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt @@ -178,6 +178,8 @@ class ChessViewModelNew( fun stopSpectating(gameId: String) = logic.stopSpectating(gameId) + fun removeGame(gameId: String) = logic.removeGame(gameId) + // ============================================ // Utility // ============================================ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyCards.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyCards.kt index e9f23f8f7..ad8124bcf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyCards.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyCards.kt @@ -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( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt index 5005d6edc..8bb9c3c92 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt @@ -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) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt index 894673718..9604fff1d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt @@ -297,7 +297,8 @@ class ChessLobbyState( } fun updatePublicGames(games: List) { - _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 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessPlayerChip.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessPlayerChip.kt index a64db869b..a363f3e70 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessPlayerChip.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessPlayerChip.kt @@ -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", diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt index 7ffedeb42..09282773e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt @@ -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(), ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index f80ba8562..3d83bdf29 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -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, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt index a09245178..b698514f3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt @@ -174,6 +174,8 @@ class DesktopChessViewModelNew( fun stopSpectating(gameId: String) = logic.stopSpectating(gameId) + fun removeGame(gameId: String) = logic.removeGame(gameId) + // ============================================ // Utility // ============================================