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:
+6
-1
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.chess.ChessBroadcastStatus
|
|||||||
import com.vitorpamplona.amethyst.commons.chess.ChessSyncBanner
|
import com.vitorpamplona.amethyst.commons.chess.ChessSyncBanner
|
||||||
import com.vitorpamplona.amethyst.commons.chess.LiveChessGameScreen
|
import com.vitorpamplona.amethyst.commons.chess.LiveChessGameScreen
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -283,7 +284,10 @@ fun ChessGameScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
Button(onClick = { nav.popBack() }) {
|
Button(onClick = {
|
||||||
|
chessViewModel.removeGame(gameId)
|
||||||
|
nav.popBack()
|
||||||
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = stringRes(R.string.back),
|
contentDescription = stringRes(R.string.back),
|
||||||
@@ -365,6 +369,7 @@ fun ChessGameScreen(
|
|||||||
blackName = blackDisplayName,
|
blackName = blackDisplayName,
|
||||||
blackHex = blackPubkey,
|
blackHex = blackPubkey,
|
||||||
blackAvatarUrl = blackAvatarUrl,
|
blackAvatarUrl = blackAvatarUrl,
|
||||||
|
onPlayerClick = { pubkeyHex -> nav.nav(Route.Profile(pubkeyHex)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -596,6 +596,7 @@ fun ChessLobbyContent(
|
|||||||
didUserWin = game.didUserWin(userPubkey),
|
didUserWin = game.didUserWin(userPubkey),
|
||||||
isDraw = game.isDraw,
|
isDraw = game.isDraw,
|
||||||
moveCount = game.moveCount,
|
moveCount = game.moveCount,
|
||||||
|
onClick = { onSelectGame(game.gameId) },
|
||||||
avatar = {
|
avatar = {
|
||||||
OverlappingAvatars(
|
OverlappingAvatars(
|
||||||
avatar1Hex = userPubkey,
|
avatar1Hex = userPubkey,
|
||||||
|
|||||||
+2
@@ -178,6 +178,8 @@ class ChessViewModelNew(
|
|||||||
|
|
||||||
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
|
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
|
||||||
|
|
||||||
|
fun removeGame(gameId: String) = logic.removeGame(gameId)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Utility
|
// Utility
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
+3
@@ -288,6 +288,7 @@ fun CompletedGameCard(
|
|||||||
didUserWin: Boolean,
|
didUserWin: Boolean,
|
||||||
isDraw: Boolean,
|
isDraw: Boolean,
|
||||||
moveCount: Int,
|
moveCount: Int,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
avatar: @Composable (() -> Unit)? = null,
|
avatar: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
@@ -309,6 +310,8 @@ fun CompletedGameCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
|
onClick = onClick ?: {},
|
||||||
|
enabled = onClick != null,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
+10
@@ -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() {
|
fun clearError() {
|
||||||
state.setError(null)
|
state.setError(null)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -297,7 +297,8 @@ class ChessLobbyState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updatePublicGames(games: List<PublicGame>) {
|
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(
|
fun addActiveGame(
|
||||||
@@ -501,7 +502,9 @@ class ChessLobbyState(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Don't add own games to spectating list
|
// 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
|
return
|
||||||
}
|
}
|
||||||
// Don't add finished games to spectating list
|
// Don't add finished games to spectating list
|
||||||
|
|||||||
+10
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.chess
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -64,6 +65,7 @@ fun ChessPlayerChip(
|
|||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
isActive: Boolean = false,
|
isActive: Boolean = false,
|
||||||
mirrored: Boolean = false,
|
mirrored: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val avatar =
|
val avatar =
|
||||||
@@ -102,7 +104,10 @@ fun ChessPlayerChip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
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,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
@@ -146,6 +151,8 @@ fun ChessPlayerVsHeader(
|
|||||||
blackHex: String,
|
blackHex: String,
|
||||||
blackAvatarUrl: String?,
|
blackAvatarUrl: String?,
|
||||||
isWhiteTurn: Boolean,
|
isWhiteTurn: Boolean,
|
||||||
|
onWhiteClick: (() -> Unit)? = null,
|
||||||
|
onBlackClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -163,6 +170,7 @@ fun ChessPlayerVsHeader(
|
|||||||
userHex = whiteHex,
|
userHex = whiteHex,
|
||||||
avatarUrl = whiteAvatarUrl,
|
avatarUrl = whiteAvatarUrl,
|
||||||
isActive = isWhiteTurn,
|
isActive = isWhiteTurn,
|
||||||
|
onClick = onWhiteClick,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "White",
|
text = "White",
|
||||||
@@ -188,6 +196,7 @@ fun ChessPlayerVsHeader(
|
|||||||
avatarUrl = blackAvatarUrl,
|
avatarUrl = blackAvatarUrl,
|
||||||
isActive = !isWhiteTurn,
|
isActive = !isWhiteTurn,
|
||||||
mirrored = true,
|
mirrored = true,
|
||||||
|
onClick = onBlackClick,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Black",
|
text = "Black",
|
||||||
|
|||||||
+3
@@ -208,6 +208,7 @@ fun LiveChessGameScreen(
|
|||||||
blackName: String = "Black",
|
blackName: String = "Black",
|
||||||
blackHex: String = "",
|
blackHex: String = "",
|
||||||
blackAvatarUrl: String? = null,
|
blackAvatarUrl: String? = null,
|
||||||
|
onPlayerClick: ((pubkeyHex: String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
// Observe state flows for automatic recomposition on updates
|
// Observe state flows for automatic recomposition on updates
|
||||||
val currentPosition by gameState.currentPosition.collectAsState()
|
val currentPosition by gameState.currentPosition.collectAsState()
|
||||||
@@ -253,6 +254,8 @@ fun LiveChessGameScreen(
|
|||||||
blackHex = blackHex,
|
blackHex = blackHex,
|
||||||
blackAvatarUrl = blackAvatarUrl,
|
blackAvatarUrl = blackAvatarUrl,
|
||||||
isWhiteTurn = currentPosition.activeColor == Color.WHITE,
|
isWhiteTurn = currentPosition.activeColor == Color.WHITE,
|
||||||
|
onWhiteClick = onPlayerClick?.let { { it(whiteHex) } },
|
||||||
|
onBlackClick = onPlayerClick?.let { { it(blackHex) } },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,8 +252,12 @@ fun ChessScreen(
|
|||||||
// Main content
|
// Main content
|
||||||
if (selectedGameId != null) {
|
if (selectedGameId != null) {
|
||||||
// Set focused game mode - only poll this game, not others
|
// 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) {
|
LaunchedEffect(selectedGameId) {
|
||||||
viewModel.setFocusedGame(selectedGameId!!)
|
viewModel.setFocusedGame(selectedGameId!!)
|
||||||
|
if (viewModel.getGameState(selectedGameId!!) == null) {
|
||||||
|
viewModel.loadGame(selectedGameId!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use stateVersion to ensure recomposition when game state changes
|
// Use stateVersion to ensure recomposition when game state changes
|
||||||
@@ -591,6 +595,7 @@ private fun ChessLobby(
|
|||||||
didUserWin = game.didUserWin(userPubkey),
|
didUserWin = game.didUserWin(userPubkey),
|
||||||
isDraw = game.isDraw,
|
isDraw = game.isDraw,
|
||||||
moveCount = game.moveCount,
|
moveCount = game.moveCount,
|
||||||
|
onClick = { onSelectGame(game.gameId) },
|
||||||
avatar = {
|
avatar = {
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
userHex = opponentPubkey,
|
userHex = opponentPubkey,
|
||||||
|
|||||||
+2
@@ -174,6 +174,8 @@ class DesktopChessViewModelNew(
|
|||||||
|
|
||||||
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
|
fun stopSpectating(gameId: String) = logic.stopSpectating(gameId)
|
||||||
|
|
||||||
|
fun removeGame(gameId: String) = logic.removeGame(gameId)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Utility
|
// Utility
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user