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:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
@@ -89,6 +89,7 @@ class FeedTopNavFilterState(
|
||||
CHESS -> {
|
||||
ChessFeedFlow(followsRelays, proxyRelays)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||
if (note != null) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -925,6 +925,7 @@ private fun RenderNoteRow(
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
is LiveChessGameChallengeEvent -> {
|
||||
RenderLiveChessChallenge(
|
||||
baseNote,
|
||||
@@ -933,6 +934,7 @@ private fun RenderNoteRow(
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
is LiveChessGameEndEvent -> {
|
||||
RenderLiveChessGameEnd(
|
||||
baseNote,
|
||||
@@ -941,6 +943,7 @@ private fun RenderNoteRow(
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
is ClassifiedsEvent -> {
|
||||
RenderClassifieds(
|
||||
noteEvent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -107,8 +107,12 @@ fun RenderLiveChessChallenge(
|
||||
|
||||
val borderColor =
|
||||
when {
|
||||
isOpenChallenge -> Color(0xFF4CAF50) // Green for open
|
||||
isIncomingChallenge -> Color(0xFFFFA726) // Orange for incoming
|
||||
isOpenChallenge -> Color(0xFF4CAF50)
|
||||
|
||||
// Green for open
|
||||
isIncomingChallenge -> Color(0xFFFFA726)
|
||||
|
||||
// Orange for incoming
|
||||
else -> MaterialTheme.colorScheme.outline // Gray for sent
|
||||
}
|
||||
|
||||
@@ -191,7 +195,7 @@ fun RenderLiveChessChallenge(
|
||||
gameId = gameId,
|
||||
challengerPubkey = challengerPubkey,
|
||||
challengerDisplayName = note.author?.toBestDisplayName(),
|
||||
challengerAvatarUrl = note.author?.info?.profilePicture(),
|
||||
challengerAvatarUrl = note.author?.profilePicture(),
|
||||
opponentPubkey = event.opponentPubkey(),
|
||||
challengerColor = event.playerColor() ?: ChessColor.WHITE,
|
||||
createdAt = event.createdAt,
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -398,12 +398,11 @@ class AndroidRelayFetcher(
|
||||
class AndroidMetadataProvider : IUserMetadataProvider {
|
||||
override fun getDisplayName(pubkey: String): String {
|
||||
val user = LocalCache.getOrCreateUser(pubkey)
|
||||
return user.info?.bestName()
|
||||
?: user.pubkeyDisplayHex()
|
||||
return user.toBestDisplayName()
|
||||
}
|
||||
|
||||
override fun getPictureUrl(pubkey: String): String? {
|
||||
val user = LocalCache.getOrCreateUser(pubkey)
|
||||
return user.info?.profilePicture()
|
||||
return user.profilePicture()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -213,9 +213,11 @@ fun ChessGameScreen(
|
||||
is ChessBroadcastStatus.Failed -> {
|
||||
// Could implement retry logic here
|
||||
}
|
||||
|
||||
is ChessBroadcastStatus.Desynced -> {
|
||||
chessViewModel.forceRefresh()
|
||||
}
|
||||
|
||||
else -> { }
|
||||
}
|
||||
},
|
||||
@@ -240,6 +242,7 @@ fun ChessGameScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameState == null -> {
|
||||
// Game not found - show error with back button
|
||||
Column(
|
||||
@@ -286,6 +289,7 @@ fun ChessGameScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Resolve opponent display name
|
||||
val opponentDisplayName =
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+2
@@ -250,9 +250,11 @@ fun HomeScreenFloatingButton(
|
||||
is LocationState.LocationResult.Loading -> { }
|
||||
}
|
||||
}
|
||||
|
||||
CHESS -> {
|
||||
NewChessGameButton(accountViewModel, nav)
|
||||
}
|
||||
|
||||
else -> {
|
||||
NewNoteButton(nav)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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 {
|
||||
private val acceptedGameIds = mutableSetOf<String>()
|
||||
private val lock = Any()
|
||||
|
||||
fun markAsAccepted(gameId: String) {
|
||||
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() {
|
||||
synchronized(lock) {
|
||||
acceptedGameIds.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove old entries - call periodically to prevent memory leak */
|
||||
fun clearOldEntries(keepGameIds: Set<String>) {
|
||||
synchronized(lock) {
|
||||
acceptedGameIds.retainAll(keepGameIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+25
-10
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -149,13 +149,18 @@ fun ChessBroadcastBanner(
|
||||
@Composable
|
||||
private fun getStatusBackgroundColor(status: ChessBroadcastStatus): Color =
|
||||
when (status) {
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced ->
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
is ChessBroadcastStatus.Success ->
|
||||
}
|
||||
|
||||
is ChessBroadcastStatus.Success -> {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else ->
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusIcon(status: ChessBroadcastStatus): ImageVector =
|
||||
when (status) {
|
||||
@@ -171,15 +176,22 @@ private fun getStatusIcon(status: ChessBroadcastStatus): ImageVector =
|
||||
@Composable
|
||||
private fun getStatusIconColor(status: ChessBroadcastStatus): Color =
|
||||
when (status) {
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced ->
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
|
||||
MaterialTheme.colorScheme.error
|
||||
is ChessBroadcastStatus.Success ->
|
||||
}
|
||||
|
||||
is ChessBroadcastStatus.Success -> {
|
||||
MaterialTheme.colorScheme.primary
|
||||
is ChessBroadcastStatus.WaitingForOpponent ->
|
||||
}
|
||||
|
||||
is ChessBroadcastStatus.WaitingForOpponent -> {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
else ->
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusText(status: ChessBroadcastStatus): String =
|
||||
when (status) {
|
||||
@@ -205,11 +217,14 @@ private fun getStatusDetail(status: ChessBroadcastStatus): String =
|
||||
@Composable
|
||||
private fun getStatusDetailColor(status: ChessBroadcastStatus): Color =
|
||||
when (status) {
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced ->
|
||||
is ChessBroadcastStatus.Failed, is ChessBroadcastStatus.Desynced -> {
|
||||
MaterialTheme.colorScheme.error
|
||||
else ->
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusProgress(status: ChessBroadcastStatus): Float? =
|
||||
when (status) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+5
-2
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
is ReconstructionResult.Error -> LoadGameResult.Error(result.message)
|
||||
|
||||
is ReconstructionResult.Error -> {
|
||||
LoadGameResult.Error(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+5
-2
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -301,7 +301,10 @@ fun CompletedGameCard(
|
||||
val resultColor =
|
||||
when {
|
||||
isDraw -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
didUserWin -> Color(0xFF4CAF50) // Green
|
||||
|
||||
didUserWin -> Color(0xFF4CAF50)
|
||||
|
||||
// Green
|
||||
else -> Color(0xFFF44336) // Red
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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.setError(null)
|
||||
}
|
||||
|
||||
is LoadGameResult.Error -> {
|
||||
state.setError("Failed to load game: ${result.message}")
|
||||
state.setBroadcastStatus(ChessBroadcastStatus.Idle)
|
||||
@@ -518,6 +519,7 @@ class ChessLobbyLogic(
|
||||
// Auto-select the game for Desktop (Android uses route navigation)
|
||||
state.selectGame(startEventId)
|
||||
}
|
||||
|
||||
is LoadGameResult.Error -> {
|
||||
state.setError("Failed to load game: ${result.message}")
|
||||
state.setBroadcastStatus(ChessBroadcastStatus.Idle)
|
||||
@@ -549,6 +551,7 @@ class ChessLobbyLogic(
|
||||
state.setBroadcastStatus(ChessBroadcastStatus.Idle)
|
||||
state.setError(null)
|
||||
}
|
||||
|
||||
is LoadGameResult.Error -> {
|
||||
// Check again if game was added while we were fetching
|
||||
// (e.g., by acceptChallenge completing in parallel)
|
||||
@@ -582,6 +585,7 @@ class ChessLobbyLogic(
|
||||
is LoadGameResult.Success -> {
|
||||
state.replaceGameState(startEventId, result.liveState)
|
||||
}
|
||||
|
||||
is LoadGameResult.Error -> {
|
||||
// Don't overwrite error for periodic refresh failures
|
||||
}
|
||||
@@ -754,6 +758,7 @@ class ChessLobbyLogic(
|
||||
pollingDelegate.addGameId(startEventId)
|
||||
}
|
||||
}
|
||||
|
||||
is LoadGameResult.Error -> {
|
||||
// Failed to load game - continue with others
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+67
-19
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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
|
||||
when {
|
||||
gameStatus is GameStatus.Finished ->
|
||||
gameStatus is GameStatus.Finished -> {
|
||||
GameEndInfo(
|
||||
result = (gameStatus as GameStatus.Finished).result,
|
||||
playerColor = gameState.playerColor,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
isSpectator -> {
|
||||
Text(
|
||||
text = "Spectating",
|
||||
@@ -437,6 +448,7 @@ private fun GameInfoHeader(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(
|
||||
text = "vs $opponentName",
|
||||
@@ -456,17 +468,26 @@ private fun GameInfoHeader(
|
||||
val resultText =
|
||||
when {
|
||||
result == GameResult.DRAW -> "Draw"
|
||||
|
||||
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
|
||||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> "You won!"
|
||||
|
||||
else -> "You lost"
|
||||
}
|
||||
val resultColor =
|
||||
when {
|
||||
result == GameResult.DRAW -> MaterialTheme.colorScheme.secondary
|
||||
result == GameResult.DRAW -> {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
|
||||
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
|
||||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) ->
|
||||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> {
|
||||
ComposeColor(0xFF4CAF50)
|
||||
else -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.error
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = resultText,
|
||||
@@ -475,6 +496,7 @@ private fun GameInfoHeader(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val turnText =
|
||||
if (currentTurn == playerColor) {
|
||||
@@ -685,21 +707,26 @@ private fun GameEndOverlay(
|
||||
}
|
||||
Quadruple("", "Game Over", winnerText, MaterialTheme.colorScheme.surfaceVariant)
|
||||
}
|
||||
playerWon ->
|
||||
|
||||
playerWon -> {
|
||||
Quadruple(
|
||||
"",
|
||||
"Victory!",
|
||||
"Congratulations!",
|
||||
ComposeColor(0xFF4CAF50).copy(alpha = 0.95f),
|
||||
)
|
||||
isDraw ->
|
||||
}
|
||||
|
||||
isDraw -> {
|
||||
Quadruple(
|
||||
"",
|
||||
"Draw",
|
||||
"Game ended in a draw",
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
else ->
|
||||
}
|
||||
|
||||
else -> {
|
||||
Quadruple(
|
||||
"",
|
||||
"Defeat",
|
||||
@@ -707,6 +734,7 @@ private fun GameEndOverlay(
|
||||
ComposeColor(0xFFE57373).copy(alpha = 0.95f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Animated visibility
|
||||
AnimatedVisibility(
|
||||
@@ -828,27 +856,47 @@ private fun GameEndInfo(
|
||||
) {
|
||||
val resultText =
|
||||
when {
|
||||
isSpectator ->
|
||||
isSpectator -> {
|
||||
when (result) {
|
||||
GameResult.WHITE_WINS -> "White wins"
|
||||
GameResult.BLACK_WINS -> "Black wins"
|
||||
GameResult.DRAW -> "Draw"
|
||||
GameResult.IN_PROGRESS -> "In progress"
|
||||
}
|
||||
result == GameResult.DRAW -> "Game drawn"
|
||||
}
|
||||
|
||||
result == GameResult.DRAW -> {
|
||||
"Game drawn"
|
||||
}
|
||||
|
||||
(result == GameResult.WHITE_WINS && playerColor == Color.WHITE) ||
|
||||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> "You won!"
|
||||
else -> "You lost"
|
||||
(result == GameResult.BLACK_WINS && playerColor == Color.BLACK) -> {
|
||||
"You won!"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"You lost"
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundColor =
|
||||
when {
|
||||
isSpectator -> MaterialTheme.colorScheme.surfaceVariant
|
||||
result == GameResult.DRAW -> MaterialTheme.colorScheme.secondaryContainer
|
||||
isSpectator -> {
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
|
||||
result == GameResult.DRAW -> {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
(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)
|
||||
else -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f)
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+10
-16
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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 subId = newSubId()
|
||||
|
||||
println("[ChessEventBroadcaster] Broadcasting event ${event.id.take(8)} to ${targetRelays.size} relays")
|
||||
|
||||
// Step 1: Check which relays are already connected
|
||||
val initialConnected = client.connectedRelaysFlow().value
|
||||
val alreadyConnected = targetRelays.intersect(initialConnected)
|
||||
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
|
||||
if (needsConnection.isNotEmpty()) {
|
||||
// Use a valid filter with recent since timestamp to trigger connection
|
||||
// without expecting real results
|
||||
val dummyFilter =
|
||||
Filter(
|
||||
kinds = listOf(JesterProtocol.KIND),
|
||||
ids = listOf("trigger_connection_${System.currentTimeMillis()}"),
|
||||
since = (System.currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results
|
||||
limit = 1,
|
||||
)
|
||||
|
||||
@@ -97,9 +95,7 @@ class ChessEventBroadcaster(
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
println("[ChessEventBroadcaster] EOSE from ${relay.url}")
|
||||
}
|
||||
) { }
|
||||
}
|
||||
|
||||
// Open subscription to all target relays (triggers connection)
|
||||
@@ -107,23 +103,21 @@ class ChessEventBroadcaster(
|
||||
client.openReqSubscription(subId, filterMap, listener)
|
||||
|
||||
// Wait for relays to connect (poll with timeout)
|
||||
val connected = waitForRelays(targetRelays, 5000L)
|
||||
println("[ChessEventBroadcaster] After waiting: ${connected.size}/${targetRelays.size} connected")
|
||||
waitForRelays(targetRelays, 5000L)
|
||||
|
||||
// Close the dummy subscription
|
||||
client.close(subId)
|
||||
}
|
||||
|
||||
// Step 3: Send the event and wait for OK responses
|
||||
println("[ChessEventBroadcaster] Sending event with sendAndWaitForResponse...")
|
||||
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(
|
||||
success = success,
|
||||
relayResults = targetRelays.associateWith { success },
|
||||
message = if (success) "Event accepted by relay(s)" else "No relay accepted the event",
|
||||
relayResults = emptyMap(),
|
||||
message = if (success) "Event accepted by at least one relay" else "No relay accepted the event",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+2
-5
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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
|
||||
if (event.kind in CHESS_EVENT_KINDS) {
|
||||
val isNew = DesktopChessEventCache.add(event)
|
||||
if (isNew) {
|
||||
println("[ChessSubscription] Cached new event: kind=${event.kind}, id=${event.id.take(8)}")
|
||||
}
|
||||
DesktopChessEventCache.add(event)
|
||||
}
|
||||
onEvent(event, isLive, relay, forFilters)
|
||||
}
|
||||
|
||||
+9
-16
@@ -201,14 +201,17 @@ fun UserProfileScreen(
|
||||
relays = configuredRelays,
|
||||
pubKeyHex = pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is MetadataEvent) {
|
||||
try {
|
||||
val content = event.content
|
||||
displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name")
|
||||
about = extractJsonField(content, "about")
|
||||
picture = extractJsonField(content, "picture")
|
||||
val metadata = event.contactMetaData()
|
||||
if (metadata != null) {
|
||||
displayName = metadata.displayName ?: metadata.name
|
||||
about = metadata.about
|
||||
picture = metadata.picture
|
||||
}
|
||||
|
||||
// Store MetadataEvent for editing (only for own profile)
|
||||
if (isOwnProfile && event is MetadataEvent) {
|
||||
if (isOwnProfile) {
|
||||
val current = latestMetadataEvent
|
||||
if (current == null || event.createdAt > current.createdAt) {
|
||||
latestMetadataEvent = event
|
||||
@@ -217,6 +220,7 @@ fun UserProfileScreen(
|
||||
} catch (e: Exception) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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
|
||||
return board.contentDeepEquals(other.board) &&
|
||||
activeColor == other.activeColor &&
|
||||
moveNumber == other.moveNumber
|
||||
moveNumber == other.moveNumber &&
|
||||
castlingRights == other.castlingRights &&
|
||||
enPassantSquare == other.enPassantSquare &&
|
||||
halfMoveClock == other.halfMoveClock
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = board.contentDeepHashCode()
|
||||
result = 31 * result + activeColor.hashCode()
|
||||
result = 31 * result + moveNumber
|
||||
result = 31 * result + castlingRights.hashCode()
|
||||
result = 31 * result + (enPassantSquare?.hashCode() ?: 0)
|
||||
result = 31 * result + halfMoveClock
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
@@ -155,14 +155,21 @@ object ChessStateReconstructor {
|
||||
val result = parseGameResult(gameResult)
|
||||
GameStatus.Finished(result)
|
||||
}
|
||||
|
||||
engine.isCheckmate() -> {
|
||||
val winner = engine.getSideToMove().opposite()
|
||||
GameStatus.Finished(
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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))
|
||||
// In a real implementation, you'd publish GameEnd event here
|
||||
}
|
||||
|
||||
engine.isStalemate() -> {
|
||||
_gameStatus.value = GameStatus.Finished(GameResult.DRAW)
|
||||
// In a real implementation, you'd publish GameEnd event here
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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")
|
||||
current.makeCastlingMove(kingSide)
|
||||
}
|
||||
|
||||
move.fromSquare != null -> {
|
||||
// Move with disambiguation (we can infer full source)
|
||||
// For now, just use simplified move
|
||||
makeSimplifiedMove(current, move)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Regular move
|
||||
makeSimplifiedMove(current, move)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
+9
-3
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* 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 sameRank = ambiguous.any { it.from.rank == fromSquare.rank }
|
||||
when {
|
||||
!sameFile -> sb.append(fileChar(fromSquare))
|
||||
!sameRank -> sb.append(rankChar(fromSquare))
|
||||
!sameFile -> {
|
||||
sb.append(fileChar(fromSquare))
|
||||
}
|
||||
|
||||
!sameRank -> {
|
||||
sb.append(rankChar(fromSquare))
|
||||
}
|
||||
|
||||
else -> {
|
||||
sb.append(fileChar(fromSquare))
|
||||
sb.append(rankChar(fromSquare))
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
Reference in New Issue
Block a user