fix: address Copilot review comments

- ChessPosition: include all fields in equals/hashCode
- AcceptedGamesRegistry: add thread safety with synchronized
- ChessEventBroadcaster: use valid filter for connection trigger
- ChessEventBroadcaster: fix misleading relayResults
- Remove println debug logging from ChessSubscription/Broadcaster
- UserProfileScreen: use proper JSON parsing via MetadataEvent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-02-10 16:34:07 +02:00
parent 8d098cf86d
commit 5f15f0c3b1
69 changed files with 255 additions and 152 deletions
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -89,6 +89,7 @@ class FeedTopNavFilterState(
CHESS -> { CHESS -> {
ChessFeedFlow(followsRelays, proxyRelays) ChessFeedFlow(followsRelays, proxyRelays)
} }
else -> { else -> {
val note = LocalCache.checkGetOrCreateAddressableNote(listName) val note = LocalCache.checkGetOrCreateAddressableNote(listName)
if (note != null) { if (note != null) {
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -925,6 +925,7 @@ private fun RenderNoteRow(
nav, nav,
) )
} }
is LiveChessGameChallengeEvent -> { is LiveChessGameChallengeEvent -> {
RenderLiveChessChallenge( RenderLiveChessChallenge(
baseNote, baseNote,
@@ -933,6 +934,7 @@ private fun RenderNoteRow(
nav, nav,
) )
} }
is LiveChessGameEndEvent -> { is LiveChessGameEndEvent -> {
RenderLiveChessGameEnd( RenderLiveChessGameEnd(
baseNote, baseNote,
@@ -941,6 +943,7 @@ private fun RenderNoteRow(
nav, nav,
) )
} }
is ClassifiedsEvent -> { is ClassifiedsEvent -> {
RenderClassifieds( RenderClassifieds(
noteEvent, noteEvent,
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -107,8 +107,12 @@ fun RenderLiveChessChallenge(
val borderColor = val borderColor =
when { when {
isOpenChallenge -> Color(0xFF4CAF50) // Green for open isOpenChallenge -> Color(0xFF4CAF50)
isIncomingChallenge -> Color(0xFFFFA726) // Orange for incoming
// Green for open
isIncomingChallenge -> Color(0xFFFFA726)
// Orange for incoming
else -> MaterialTheme.colorScheme.outline // Gray for sent else -> MaterialTheme.colorScheme.outline // Gray for sent
} }
@@ -191,7 +195,7 @@ fun RenderLiveChessChallenge(
gameId = gameId, gameId = gameId,
challengerPubkey = challengerPubkey, challengerPubkey = challengerPubkey,
challengerDisplayName = note.author?.toBestDisplayName(), challengerDisplayName = note.author?.toBestDisplayName(),
challengerAvatarUrl = note.author?.info?.profilePicture(), challengerAvatarUrl = note.author?.profilePicture(),
opponentPubkey = event.opponentPubkey(), opponentPubkey = event.opponentPubkey(),
challengerColor = event.playerColor() ?: ChessColor.WHITE, challengerColor = event.playerColor() ?: ChessColor.WHITE,
createdAt = event.createdAt, createdAt = event.createdAt,
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -398,12 +398,11 @@ class AndroidRelayFetcher(
class AndroidMetadataProvider : IUserMetadataProvider { class AndroidMetadataProvider : IUserMetadataProvider {
override fun getDisplayName(pubkey: String): String { override fun getDisplayName(pubkey: String): String {
val user = LocalCache.getOrCreateUser(pubkey) val user = LocalCache.getOrCreateUser(pubkey)
return user.info?.bestName() return user.toBestDisplayName()
?: user.pubkeyDisplayHex()
} }
override fun getPictureUrl(pubkey: String): String? { override fun getPictureUrl(pubkey: String): String? {
val user = LocalCache.getOrCreateUser(pubkey) val user = LocalCache.getOrCreateUser(pubkey)
return user.info?.profilePicture() return user.profilePicture()
} }
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -213,9 +213,11 @@ fun ChessGameScreen(
is ChessBroadcastStatus.Failed -> { is ChessBroadcastStatus.Failed -> {
// Could implement retry logic here // Could implement retry logic here
} }
is ChessBroadcastStatus.Desynced -> { is ChessBroadcastStatus.Desynced -> {
chessViewModel.forceRefresh() chessViewModel.forceRefresh()
} }
else -> { } else -> { }
} }
}, },
@@ -240,6 +242,7 @@ fun ChessGameScreen(
} }
} }
} }
gameState == null -> { gameState == null -> {
// Game not found - show error with back button // Game not found - show error with back button
Column( Column(
@@ -286,6 +289,7 @@ fun ChessGameScreen(
} }
} }
} }
else -> { else -> {
// Resolve opponent display name // Resolve opponent display name
val opponentDisplayName = val opponentDisplayName =
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -250,9 +250,11 @@ fun HomeScreenFloatingButton(
is LocationState.LocationResult.Loading -> { } is LocationState.LocationResult.Loading -> { }
} }
} }
CHESS -> { CHESS -> {
NewChessGameButton(accountViewModel, nav) NewChessGameButton(accountViewModel, nav)
} }
else -> { else -> {
NewNoteButton(nav) NewNoteButton(nav)
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -29,19 +29,29 @@ package com.vitorpamplona.amethyst.commons.chess
*/ */
object AcceptedGamesRegistry { object AcceptedGamesRegistry {
private val acceptedGameIds = mutableSetOf<String>() private val acceptedGameIds = mutableSetOf<String>()
private val lock = Any()
fun markAsAccepted(gameId: String) { fun markAsAccepted(gameId: String) {
acceptedGameIds.add(gameId) synchronized(lock) {
acceptedGameIds.add(gameId)
}
} }
fun wasAccepted(gameId: String): Boolean = acceptedGameIds.contains(gameId) fun wasAccepted(gameId: String): Boolean =
synchronized(lock) {
acceptedGameIds.contains(gameId)
}
fun clear() { fun clear() {
acceptedGameIds.clear() synchronized(lock) {
acceptedGameIds.clear()
}
} }
/** Remove old entries - call periodically to prevent memory leak */ /** Remove old entries - call periodically to prevent memory leak */
fun clearOldEntries(keepGameIds: Set<String>) { fun clearOldEntries(keepGameIds: Set<String>) {
acceptedGameIds.retainAll(keepGameIds) synchronized(lock) {
acceptedGameIds.retainAll(keepGameIds)
}
} }
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -149,12 +149,17 @@ fun ChessBroadcastBanner(
@Composable @Composable
private fun getStatusBackgroundColor(status: ChessBroadcastStatus): Color = private fun getStatusBackgroundColor(status: ChessBroadcastStatus): Color =
when (status) { when (status) {
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
MaterialTheme.colorScheme.errorContainer MaterialTheme.colorScheme.errorContainer
is ChessBroadcastStatus.Success -> }
is ChessBroadcastStatus.Success -> {
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
else -> }
else -> {
MaterialTheme.colorScheme.surfaceContainer MaterialTheme.colorScheme.surfaceContainer
}
} }
private fun getStatusIcon(status: ChessBroadcastStatus): ImageVector = private fun getStatusIcon(status: ChessBroadcastStatus): ImageVector =
@@ -171,14 +176,21 @@ private fun getStatusIcon(status: ChessBroadcastStatus): ImageVector =
@Composable @Composable
private fun getStatusIconColor(status: ChessBroadcastStatus): Color = private fun getStatusIconColor(status: ChessBroadcastStatus): Color =
when (status) { when (status) {
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
is ChessBroadcastStatus.Success -> }
is ChessBroadcastStatus.Success -> {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
is ChessBroadcastStatus.WaitingForOpponent -> }
is ChessBroadcastStatus.WaitingForOpponent -> {
MaterialTheme.colorScheme.secondary MaterialTheme.colorScheme.secondary
else -> }
else -> {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
}
} }
private fun getStatusText(status: ChessBroadcastStatus): String = private fun getStatusText(status: ChessBroadcastStatus): String =
@@ -205,10 +217,13 @@ private fun getStatusDetail(status: ChessBroadcastStatus): String =
@Composable @Composable
private fun getStatusDetailColor(status: ChessBroadcastStatus): Color = private fun getStatusDetailColor(status: ChessBroadcastStatus): Color =
when (status) { when (status) {
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else -> }
else -> {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
}
} }
private fun getStatusProgress(status: ChessBroadcastStatus): Float? = private fun getStatusProgress(status: ChessBroadcastStatus): Float? =
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -126,7 +126,10 @@ object ChessGameLoader {
LoadGameResult.Error("Failed to convert reconstructed state to live state") LoadGameResult.Error("Failed to convert reconstructed state to live state")
} }
} }
is ReconstructionResult.Error -> LoadGameResult.Error(result.message)
is ReconstructionResult.Error -> {
LoadGameResult.Error(result.message)
}
} }
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -301,7 +301,10 @@ fun CompletedGameCard(
val resultColor = val resultColor =
when { when {
isDraw -> MaterialTheme.colorScheme.onSurfaceVariant isDraw -> MaterialTheme.colorScheme.onSurfaceVariant
didUserWin -> Color(0xFF4CAF50) // Green
didUserWin -> Color(0xFF4CAF50)
// Green
else -> Color(0xFFF44336) // Red else -> Color(0xFFF44336) // Red
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -389,6 +389,7 @@ class ChessLobbyLogic(
state.setBroadcastStatus(ChessBroadcastStatus.Idle) state.setBroadcastStatus(ChessBroadcastStatus.Idle)
state.setError(null) state.setError(null)
} }
is LoadGameResult.Error -> { is LoadGameResult.Error -> {
state.setError("Failed to load game: ${result.message}") state.setError("Failed to load game: ${result.message}")
state.setBroadcastStatus(ChessBroadcastStatus.Idle) state.setBroadcastStatus(ChessBroadcastStatus.Idle)
@@ -518,6 +519,7 @@ class ChessLobbyLogic(
// Auto-select the game for Desktop (Android uses route navigation) // Auto-select the game for Desktop (Android uses route navigation)
state.selectGame(startEventId) state.selectGame(startEventId)
} }
is LoadGameResult.Error -> { is LoadGameResult.Error -> {
state.setError("Failed to load game: ${result.message}") state.setError("Failed to load game: ${result.message}")
state.setBroadcastStatus(ChessBroadcastStatus.Idle) state.setBroadcastStatus(ChessBroadcastStatus.Idle)
@@ -549,6 +551,7 @@ class ChessLobbyLogic(
state.setBroadcastStatus(ChessBroadcastStatus.Idle) state.setBroadcastStatus(ChessBroadcastStatus.Idle)
state.setError(null) state.setError(null)
} }
is LoadGameResult.Error -> { is LoadGameResult.Error -> {
// Check again if game was added while we were fetching // Check again if game was added while we were fetching
// (e.g., by acceptChallenge completing in parallel) // (e.g., by acceptChallenge completing in parallel)
@@ -582,6 +585,7 @@ class ChessLobbyLogic(
is LoadGameResult.Success -> { is LoadGameResult.Success -> {
state.replaceGameState(startEventId, result.liveState) state.replaceGameState(startEventId, result.liveState)
} }
is LoadGameResult.Error -> { is LoadGameResult.Error -> {
// Don't overwrite error for periodic refresh failures // Don't overwrite error for periodic refresh failures
} }
@@ -754,6 +758,7 @@ class ChessLobbyLogic(
pollingDelegate.addGameId(startEventId) pollingDelegate.addGameId(startEventId)
} }
} }
is LoadGameResult.Error -> { is LoadGameResult.Error -> {
// Failed to load game - continue with others // Failed to load game - continue with others
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -266,15 +266,25 @@ fun LiveChessGameScreen(
// Show appropriate controls based on game state // Show appropriate controls based on game state
when { when {
gameStatus is GameStatus.Finished -> gameStatus is GameStatus.Finished -> {
GameEndInfo( GameEndInfo(
result = (gameStatus as GameStatus.Finished).result, result = (gameStatus as GameStatus.Finished).result,
playerColor = gameState.playerColor, playerColor = gameState.playerColor,
isSpectator = isSpectator, isSpectator = isSpectator,
) )
gameState.isPendingChallenge -> PendingChallengeInfo() }
isSpectator -> SpectatorInfo()
else -> GameControls(onResign = onResign) gameState.isPendingChallenge -> {
PendingChallengeInfo()
}
isSpectator -> {
SpectatorInfo()
}
else -> {
GameControls(onResign = onResign)
}
} }
} }
} }
@@ -422,6 +432,7 @@ private fun GameInfoHeader(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
isSpectator -> { isSpectator -> {
Text( Text(
text = "Spectating", text = "Spectating",
@@ -437,6 +448,7 @@ private fun GameInfoHeader(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
else -> { else -> {
Text( Text(
text = "vs $opponentName", text = "vs $opponentName",
@@ -456,17 +468,26 @@ private fun GameInfoHeader(
val resultText = val resultText =
when { when {
result == GameResult.DRAW -> "Draw" result == GameResult.DRAW -> "Draw"
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) || (result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> "You won!" (result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> "You won!"
else -> "You lost" else -> "You lost"
} }
val resultColor = val resultColor =
when { when {
result == GameResult.DRAW -> MaterialTheme.colorScheme.secondary result == GameResult.DRAW -> {
MaterialTheme.colorScheme.secondary
}
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) || (result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> (result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> {
ComposeColor(0xFF4CAF50) ComposeColor(0xFF4CAF50)
else -> MaterialTheme.colorScheme.error }
else -> {
MaterialTheme.colorScheme.error
}
} }
Text( Text(
text = resultText, text = resultText,
@@ -475,6 +496,7 @@ private fun GameInfoHeader(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
} }
else -> { else -> {
val turnText = val turnText =
if (currentTurn == playerColor) { if (currentTurn == playerColor) {
@@ -685,27 +707,33 @@ private fun GameEndOverlay(
} }
Quadruple("", "Game Over", winnerText, MaterialTheme.colorScheme.surfaceVariant) Quadruple("", "Game Over", winnerText, MaterialTheme.colorScheme.surfaceVariant)
} }
playerWon ->
playerWon -> {
Quadruple( Quadruple(
"", "",
"Victory!", "Victory!",
"Congratulations!", "Congratulations!",
ComposeColor(0xFF4CAF50).copy(alpha = 0.95f), ComposeColor(0xFF4CAF50).copy(alpha = 0.95f),
) )
isDraw -> }
isDraw -> {
Quadruple( Quadruple(
"", "",
"Draw", "Draw",
"Game ended in a draw", "Game ended in a draw",
MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.surfaceVariant,
) )
else -> }
else -> {
Quadruple( Quadruple(
"", "",
"Defeat", "Defeat",
"Better luck next time!", "Better luck next time!",
ComposeColor(0xFFE57373).copy(alpha = 0.95f), ComposeColor(0xFFE57373).copy(alpha = 0.95f),
) )
}
} }
// Animated visibility // Animated visibility
@@ -828,27 +856,47 @@ private fun GameEndInfo(
) { ) {
val resultText = val resultText =
when { when {
isSpectator -> isSpectator -> {
when (result) { when (result) {
GameResult.WHITE_WINS -> "White wins" GameResult.WHITE_WINS -> "White wins"
GameResult.BLACK_WINS -> "Black wins" GameResult.BLACK_WINS -> "Black wins"
GameResult.DRAW -> "Draw" GameResult.DRAW -> "Draw"
GameResult.IN_PROGRESS -> "In progress" GameResult.IN_PROGRESS -> "In progress"
} }
result == GameResult.DRAW -> "Game drawn" }
result == GameResult.DRAW -> {
"Game drawn"
}
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) || (result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> "You won!" (result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> {
else -> "You lost" "You won!"
}
else -> {
"You lost"
}
} }
val backgroundColor = val backgroundColor =
when { when {
isSpectator -> MaterialTheme.colorScheme.surfaceVariant isSpectator -> {
result == GameResult.DRAW -> MaterialTheme.colorScheme.secondaryContainer MaterialTheme.colorScheme.surfaceVariant
}
result == GameResult.DRAW -> {
MaterialTheme.colorScheme.secondaryContainer
}
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) || (result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> (result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> {
ComposeColor(0xFF4CAF50).copy(alpha = 0.3f) ComposeColor(0xFF4CAF50).copy(alpha = 0.3f)
else -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) }
else -> {
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f)
}
} }
Box( Box(
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -67,21 +67,19 @@ class ChessEventBroadcaster(
val targetRelays = ChessConfig.CHESS_RELAYS.map { NormalizedRelayUrl(it) }.toSet() val targetRelays = ChessConfig.CHESS_RELAYS.map { NormalizedRelayUrl(it) }.toSet()
val subId = newSubId() val subId = newSubId()
println("[ChessEventBroadcaster] Broadcasting event ${event.id.take(8)} to ${targetRelays.size} relays")
// Step 1: Check which relays are already connected // Step 1: Check which relays are already connected
val initialConnected = client.connectedRelaysFlow().value val initialConnected = client.connectedRelaysFlow().value
val alreadyConnected = targetRelays.intersect(initialConnected) val alreadyConnected = targetRelays.intersect(initialConnected)
val needsConnection = targetRelays - alreadyConnected val needsConnection = targetRelays - alreadyConnected
println("[ChessEventBroadcaster] Already connected: ${alreadyConnected.size}, needs connection: ${needsConnection.size}")
// Step 2: If some relays need connection, open a subscription to trigger it // Step 2: If some relays need connection, open a subscription to trigger it
if (needsConnection.isNotEmpty()) { if (needsConnection.isNotEmpty()) {
// Use a valid filter with recent since timestamp to trigger connection
// without expecting real results
val dummyFilter = val dummyFilter =
Filter( Filter(
kinds = listOf(JesterProtocol.KIND), kinds = listOf(JesterProtocol.KIND),
ids = listOf("trigger_connection_${System.currentTimeMillis()}"), since = (System.currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results
limit = 1, limit = 1,
) )
@@ -97,9 +95,7 @@ class ChessEventBroadcaster(
override fun onEose( override fun onEose(
relay: NormalizedRelayUrl, relay: NormalizedRelayUrl,
forFilters: List<Filter>?, forFilters: List<Filter>?,
) { ) { }
println("[ChessEventBroadcaster] EOSE from ${relay.url}")
}
} }
// Open subscription to all target relays (triggers connection) // Open subscription to all target relays (triggers connection)
@@ -107,23 +103,21 @@ class ChessEventBroadcaster(
client.openReqSubscription(subId, filterMap, listener) client.openReqSubscription(subId, filterMap, listener)
// Wait for relays to connect (poll with timeout) // Wait for relays to connect (poll with timeout)
val connected = waitForRelays(targetRelays, 5000L) waitForRelays(targetRelays, 5000L)
println("[ChessEventBroadcaster] After waiting: ${connected.size}/${targetRelays.size} connected")
// Close the dummy subscription // Close the dummy subscription
client.close(subId) client.close(subId)
} }
// Step 3: Send the event and wait for OK responses // Step 3: Send the event and wait for OK responses
println("[ChessEventBroadcaster] Sending event with sendAndWaitForResponse...")
val success = client.sendAndWaitForResponse(event, targetRelays, timeoutSeconds) val success = client.sendAndWaitForResponse(event, targetRelays, timeoutSeconds)
println("[ChessEventBroadcaster] Broadcast complete: success=$success") // Note: sendAndWaitForResponse only returns aggregate success (any relay accepted)
// We don't have per-relay results, so relayResults is empty
return BroadcastResult( return BroadcastResult(
success = success, success = success,
relayResults = targetRelays.associateWith { success }, relayResults = emptyMap(),
message = if (success) "Event accepted by relay(s)" else "No relay accepted the event", message = if (success) "Event accepted by at least one relay" else "No relay accepted the event",
) )
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -104,10 +104,7 @@ class DesktopChessSubscriptionController(
) { ) {
// Add chess events to local cache for persistence // Add chess events to local cache for persistence
if (event.kind in CHESS_EVENT_KINDS) { if (event.kind in CHESS_EVENT_KINDS) {
val isNew = DesktopChessEventCache.add(event) DesktopChessEventCache.add(event)
if (isNew) {
println("[ChessSubscription] Cached new event: kind=${event.kind}, id=${event.id.take(8)}")
}
} }
onEvent(event, isLive, relay, forFilters) onEvent(event, isLive, relay, forFilters)
} }
@@ -201,21 +201,25 @@ fun UserProfileScreen(
relays = configuredRelays, relays = configuredRelays,
pubKeyHex = pubKeyHex, pubKeyHex = pubKeyHex,
onEvent = { event, _, _, _ -> onEvent = { event, _, _, _ ->
try { if (event is MetadataEvent) {
val content = event.content try {
displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name") val metadata = event.contactMetaData()
about = extractJsonField(content, "about") if (metadata != null) {
picture = extractJsonField(content, "picture") displayName = metadata.displayName ?: metadata.name
about = metadata.about
// Store MetadataEvent for editing (only for own profile) picture = metadata.picture
if (isOwnProfile && event is MetadataEvent) {
val current = latestMetadataEvent
if (current == null || event.createdAt > current.createdAt) {
latestMetadataEvent = event
} }
// Store MetadataEvent for editing (only for own profile)
if (isOwnProfile) {
val current = latestMetadataEvent
if (current == null || event.createdAt > current.createdAt) {
latestMetadataEvent = event
}
}
} catch (e: Exception) {
// Ignore parse errors
} }
} catch (e: Exception) {
// Ignore parse errors
} }
}, },
) )
@@ -688,17 +692,6 @@ fun UserProfileScreen(
} }
} }
/**
* Simple JSON field extractor (not production-ready, just for demo).
*/
private fun extractJsonField(
json: String,
field: String,
): String? {
val regex = """"$field"\s*:\s*"([^"]*)"""".toRegex()
return regex.find(json)?.groupValues?.get(1)
}
/** /**
* Follows a user by publishing an updated contact list event. * Follows a user by publishing an updated contact list event.
*/ */
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -177,13 +177,19 @@ data class ChessPosition(
if (other !is ChessPosition) return false if (other !is ChessPosition) return false
return board.contentDeepEquals(other.board) && return board.contentDeepEquals(other.board) &&
activeColor == other.activeColor && activeColor == other.activeColor &&
moveNumber == other.moveNumber moveNumber == other.moveNumber &&
castlingRights == other.castlingRights &&
enPassantSquare == other.enPassantSquare &&
halfMoveClock == other.halfMoveClock
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = board.contentDeepHashCode() var result = board.contentDeepHashCode()
result = 31 * result + activeColor.hashCode() result = 31 * result + activeColor.hashCode()
result = 31 * result + moveNumber result = 31 * result + moveNumber
result = 31 * result + castlingRights.hashCode()
result = 31 * result + (enPassantSquare?.hashCode() ?: 0)
result = 31 * result + halfMoveClock
return result return result
} }
} }
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -155,14 +155,21 @@ object ChessStateReconstructor {
val result = parseGameResult(gameResult) val result = parseGameResult(gameResult)
GameStatus.Finished(result) GameStatus.Finished(result)
} }
engine.isCheckmate() -> { engine.isCheckmate() -> {
val winner = engine.getSideToMove().opposite() val winner = engine.getSideToMove().opposite()
GameStatus.Finished( GameStatus.Finished(
if (winner == Color.WHITE) GameResult.WHITE_WINS else GameResult.BLACK_WINS, if (winner == Color.WHITE) GameResult.WHITE_WINS else GameResult.BLACK_WINS,
) )
} }
engine.isStalemate() -> GameStatus.Finished(GameResult.DRAW)
else -> GameStatus.InProgress engine.isStalemate() -> {
GameStatus.Finished(GameResult.DRAW)
}
else -> {
GameStatus.InProgress
}
} }
// Get the head event ID (for linking next move) // Get the head event ID (for linking next move)
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -464,6 +464,7 @@ class LiveChessGameState(
_gameStatus.value = GameStatus.Finished(GameResult.getResultForWinner(winner)) _gameStatus.value = GameStatus.Finished(GameResult.getResultForWinner(winner))
// In a real implementation, you'd publish GameEnd event here // In a real implementation, you'd publish GameEnd event here
} }
engine.isStalemate() -> { engine.isStalemate() -> {
_gameStatus.value = GameStatus.Finished(GameResult.DRAW) _gameStatus.value = GameStatus.Finished(GameResult.DRAW)
// In a real implementation, you'd publish GameEnd event here // In a real implementation, you'd publish GameEnd event here
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -258,11 +258,13 @@ object PGNParser {
val kingSide = move.toSquare.contains("g") val kingSide = move.toSquare.contains("g")
current.makeCastlingMove(kingSide) current.makeCastlingMove(kingSide)
} }
move.fromSquare != null -> { move.fromSquare != null -> {
// Move with disambiguation (we can infer full source) // Move with disambiguation (we can infer full source)
// For now, just use simplified move // For now, just use simplified move
makeSimplifiedMove(current, move) makeSimplifiedMove(current, move)
} }
else -> { else -> {
// Regular move // Regular move
makeSimplifiedMove(current, move) makeSimplifiedMove(current, move)
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -231,8 +231,14 @@ actual class ChessEngine {
val sameFile = ambiguous.any { it.from.file == fromSquare.file } val sameFile = ambiguous.any { it.from.file == fromSquare.file }
val sameRank = ambiguous.any { it.from.rank == fromSquare.rank } val sameRank = ambiguous.any { it.from.rank == fromSquare.rank }
when { when {
!sameFile -> sb.append(fileChar(fromSquare)) !sameFile -> {
!sameRank -> sb.append(rankChar(fromSquare)) sb.append(fileChar(fromSquare))
}
!sameRank -> {
sb.append(rankChar(fromSquare))
}
else -> { else -> {
sb.append(fileChar(fromSquare)) sb.append(fileChar(fromSquare))
sb.append(rankChar(fromSquare)) sb.append(rankChar(fromSquare))
@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2025 Vitor Pamplona * Copyright (c) 2025 Vitor Pamplona
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of * Permission is hereby granted, free of charge, to any person obtaining a copy of