feat: replace hex pubkeys with user picture + name in chess cards
In chess note cards (NoteCompose), player hex keys were displayed as raw strings. Now uses LoadUser + ClickableUserPicture + UsernameDisplay to show proper profile pictures and display names for: - Challenge cards (incoming/outgoing) - Game end cards (both players) - PGN metadata in game viewers (white/black players) Added playerContent composable slot to PGNMetadata and ChessGameViewer so callers can inject platform-specific user rendering. https://claude.ai/code/session_0171mKrVEfQnNRabmT7Kv4gf
This commit is contained in:
@@ -37,6 +37,7 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -46,10 +47,15 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.commons.chess.ChessChallenge
|
import com.vitorpamplona.amethyst.commons.chess.ChessChallenge
|
||||||
import com.vitorpamplona.amethyst.commons.chess.ChessGameViewer
|
import com.vitorpamplona.amethyst.commons.chess.ChessGameViewer
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||||
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.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -79,7 +85,22 @@ fun RenderChessGame(
|
|||||||
val event = (note.event as? ChessGameEvent) ?: return
|
val event = (note.event as? ChessGameEvent) ?: return
|
||||||
|
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||||
ChessGameViewer(pgnContent = event.pgn())
|
ChessGameViewer(
|
||||||
|
pgnContent = event.pgn(),
|
||||||
|
playerContent = { playerHex ->
|
||||||
|
LoadUser(playerHex, accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UserChip(user = user, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = playerHex.take(8) + "...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,20 +141,6 @@ fun RenderLiveChessChallenge(
|
|||||||
else -> MaterialTheme.colorScheme.outline // Gray for sent
|
else -> MaterialTheme.colorScheme.outline // Gray for sent
|
||||||
}
|
}
|
||||||
|
|
||||||
val icon =
|
|
||||||
when {
|
|
||||||
isOpenChallenge -> "🔓"
|
|
||||||
isIncomingChallenge -> "💌"
|
|
||||||
else -> "⏳"
|
|
||||||
}
|
|
||||||
|
|
||||||
val title =
|
|
||||||
when {
|
|
||||||
isOpenChallenge -> "Open Challenge"
|
|
||||||
isIncomingChallenge -> "Challenge from ${note.author?.toBestDisplayName()}"
|
|
||||||
else -> "Challenge sent to ${event.opponentPubkey()?.take(8)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -152,12 +159,49 @@ fun RenderLiveChessChallenge(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = "$icon $title",
|
modifier = Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontWeight = FontWeight.Bold,
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
)
|
) {
|
||||||
|
when {
|
||||||
|
isOpenChallenge -> {
|
||||||
|
Text(
|
||||||
|
text = "🔓 Open Challenge",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isIncomingChallenge -> {
|
||||||
|
Text(
|
||||||
|
text = "💌 Challenge from",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
note.author?.let { author ->
|
||||||
|
UserChip(user = author, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "⏳ Challenge sent to",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
event.opponentPubkey()?.let { opponentHex ->
|
||||||
|
LoadUser(opponentHex, accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UserChip(user = user, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
@@ -249,6 +293,31 @@ fun RenderLiveChessGameEnd(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Players row
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
note.author?.let { author ->
|
||||||
|
UserChip(user = author, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "vs",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.opponentPubkey()?.let { opponentHex ->
|
||||||
|
LoadUser(opponentHex, accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UserChip(user = user, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Result: ${event.result()}",
|
text = "Result: ${event.result()}",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -265,8 +334,49 @@ fun RenderLiveChessGameEnd(
|
|||||||
|
|
||||||
// Show PGN if available
|
// Show PGN if available
|
||||||
event.pgn().takeIf { it.isNotBlank() }?.let { pgn ->
|
event.pgn().takeIf { it.isNotBlank() }?.let { pgn ->
|
||||||
ChessGameViewer(pgnContent = pgn)
|
ChessGameViewer(
|
||||||
|
pgnContent = pgn,
|
||||||
|
playerContent = { playerHex ->
|
||||||
|
LoadUser(playerHex, accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UserChip(user = user, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = playerHex.take(8) + "...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact user display with small picture and name, shown inline.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun UserChip(
|
||||||
|
user: User,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
ClickableUserPicture(
|
||||||
|
baseUser = user,
|
||||||
|
size = 24.dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onClick = { nav.nav(routeFor(it)) },
|
||||||
|
)
|
||||||
|
UsernameDisplay(
|
||||||
|
baseUser = user,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-2
@@ -51,11 +51,14 @@ import com.vitorpamplona.quartz.nip64Chess.PGNParser
|
|||||||
*
|
*
|
||||||
* @param pgnContent PGN format string
|
* @param pgnContent PGN format string
|
||||||
* @param modifier Modifier for the viewer
|
* @param modifier Modifier for the viewer
|
||||||
|
* @param playerContent Optional composable to render player names with avatar + display name.
|
||||||
|
* When provided, PGN player names (which may be hex pubkeys) are rendered using this composable.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ChessGameViewer(
|
fun ChessGameViewer(
|
||||||
pgnContent: String,
|
pgnContent: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
playerContent: (@Composable (String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val gameResult =
|
val gameResult =
|
||||||
remember(pgnContent) {
|
remember(pgnContent) {
|
||||||
@@ -64,7 +67,7 @@ fun ChessGameViewer(
|
|||||||
|
|
||||||
gameResult.fold(
|
gameResult.fold(
|
||||||
onSuccess = { game ->
|
onSuccess = { game ->
|
||||||
ChessGameDisplay(game, modifier)
|
ChessGameDisplay(game, modifier, playerContent)
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
ChessGameError(
|
ChessGameError(
|
||||||
@@ -83,6 +86,7 @@ fun ChessGameViewer(
|
|||||||
private fun ChessGameDisplay(
|
private fun ChessGameDisplay(
|
||||||
game: com.vitorpamplona.quartz.nip64Chess.ChessGame,
|
game: com.vitorpamplona.quartz.nip64Chess.ChessGame,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
playerContent: (@Composable (String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var currentMoveIndex by remember { mutableStateOf(0) }
|
var currentMoveIndex by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ private fun ChessGameDisplay(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
// Game metadata header
|
// Game metadata header
|
||||||
PGNMetadata(game = game)
|
PGNMetadata(game = game, playerContent = playerContent)
|
||||||
|
|
||||||
// Chess board
|
// Chess board
|
||||||
ChessBoard(
|
ChessBoard(
|
||||||
|
|||||||
+24
-12
@@ -39,11 +39,15 @@ import com.vitorpamplona.quartz.nip64Chess.ChessGame
|
|||||||
*
|
*
|
||||||
* @param game The chess game with metadata to display
|
* @param game The chess game with metadata to display
|
||||||
* @param modifier Modifier for the metadata display
|
* @param modifier Modifier for the metadata display
|
||||||
|
* @param playerContent Optional composable to render player names (e.g. with avatar + display name).
|
||||||
|
* Receives the raw player name string from PGN (which may be a hex pubkey in Nostr context).
|
||||||
|
* When null, falls back to plain text display.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun PGNMetadata(
|
fun PGNMetadata(
|
||||||
game: ChessGame,
|
game: ChessGame,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
playerContent: (@Composable (String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
@@ -76,12 +80,16 @@ fun PGNMetadata(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
game.white?.let { white ->
|
game.white?.let { white ->
|
||||||
Text(
|
if (playerContent != null) {
|
||||||
text = white,
|
playerContent(white)
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
} else {
|
||||||
fontWeight = FontWeight.Medium,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
text = white,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -91,12 +99,16 @@ fun PGNMetadata(
|
|||||||
)
|
)
|
||||||
|
|
||||||
game.black?.let { black ->
|
game.black?.let { black ->
|
||||||
Text(
|
if (playerContent != null) {
|
||||||
text = black,
|
playerContent(black)
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
} else {
|
||||||
fontWeight = FontWeight.Medium,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
text = black,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user