refactor: Polish shared commons components and fix formatting

- Improve RelayConnectionManager with better connection handling
- Refine UI components (LoginCard, ProfileInfoCard, RelayStatusCard)
- Update KeyInputField with improved layout
- Fix formatting in FeedHeader and AppScreen
- Add proper number formatting utilities
- Remove .claude config from version control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2025-12-28 07:03:57 +02:00
parent 201acc8140
commit b23ff488ed
21 changed files with 152 additions and 113 deletions
@@ -31,6 +31,7 @@ import coil3.fetch.ImageFetchResult
import coil3.key.Keyer
import coil3.request.Options
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image
import com.vitorpamplona.amethyst.commons.base64Image.toBitmap
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.sha256.sha256
@@ -42,7 +43,7 @@ class Base64Fetcher(
override suspend fun fetch(): FetchResult? =
runCatching {
ImageFetchResult(
image = Base64Image.Companion.toBitmap(data.toString()).asImage(true),
image = Base64Image.toBitmap(data.toString()).asImage(true),
isSampled = false,
dataSource = DataSource.MEMORY,
)
@@ -30,6 +30,7 @@ import coil3.fetch.ImageFetchResult
import coil3.key.Keyer
import coil3.request.Options
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
import com.vitorpamplona.amethyst.commons.blurhash.toAndroidBitmap
data class BlurhashWrapper(
val blurhash: String,
@@ -43,10 +44,10 @@ class BlurHashFetcher(
override suspend fun fetch(): FetchResult? {
val hash = data.blurhash
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
val platformImage = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
return ImageFetchResult(
image = bitmap.asImage(true),
image = platformImage.toAndroidBitmap().asImage(true),
isSampled = false,
dataSource = DataSource.MEMORY,
)
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -39,17 +39,19 @@ enum class AppScreen(
/**
* Primary navigation destinations (shown in bottom bar on mobile, sidebar on desktop).
*/
val primaryScreens = listOf(
AppScreen.Feed,
AppScreen.Search,
AppScreen.Messages,
AppScreen.Notifications,
AppScreen.Profile,
)
val primaryScreens =
listOf(
AppScreen.Feed,
AppScreen.Search,
AppScreen.Messages,
AppScreen.Notifications,
AppScreen.Profile,
)
/**
* Secondary navigation destinations (settings, etc.)
*/
val secondaryScreens = listOf(
AppScreen.Settings,
)
val secondaryScreens =
listOf(
AppScreen.Settings,
)
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -35,7 +35,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.theme.RelayStatusColors
@@ -57,12 +56,12 @@ fun FeedHeader(
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
Text(
title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
RelayStatusIndicator(
@@ -84,32 +83,33 @@ fun RelayStatusIndicator(
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
val statusColor =
when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = statusColor,
modifier = Modifier.size(16.dp)
modifier = Modifier.size(16.dp),
)
Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodySmall,
)
IconButton(onClick = onRefresh) {
Icon(
Icons.Default.Refresh,
contentDescription = "Reconnect",
tint = MaterialTheme.colorScheme.primary
tint = MaterialTheme.colorScheme.primary,
)
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -42,12 +42,12 @@ fun PlaceholderScreen(
Text(
title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(Modifier.height(16.dp))
Text(
description,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -26,7 +26,10 @@ import kotlin.math.roundToInt
* Formats a count to human-readable format with suffix (K, M, G).
* Example: 1500 -> "1K items", 2500000 -> "2M items"
*/
fun countToHumanReadable(counter: Int, suffix: String): String =
fun countToHumanReadable(
counter: Int,
suffix: String,
): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $suffix"
@@ -37,7 +40,10 @@ fun countToHumanReadable(counter: Int, suffix: String): String =
/**
* Formats a count to human-readable format with suffix (K, M, G).
*/
fun countToHumanReadable(counter: Long, suffix: String): String =
fun countToHumanReadable(
counter: Long,
suffix: String,
): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $suffix"
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -92,7 +92,10 @@ open class RelayConnectionManager(
client.close(subId)
}
fun send(event: Event, relays: Set<NormalizedRelayUrl> = connectedRelays.value) {
fun send(
event: Event,
relays: Set<NormalizedRelayUrl> = connectedRelays.value,
) {
client.send(event, relays)
}
@@ -100,10 +103,11 @@ open class RelayConnectionManager(
url: NormalizedRelayUrl,
update: (RelayStatus) -> RelayStatus,
) {
_relayStatuses.value = _relayStatuses.value.toMutableMap().apply {
val current = this[url] ?: RelayStatus(url, connected = false)
this[url] = update(current)
}
_relayStatuses.value =
_relayStatuses.value.toMutableMap().apply {
val current = this[url] ?: RelayStatus(url, connected = false)
this[url] = update(current)
}
}
override fun onConnecting(relay: IRelayClient) {
@@ -112,7 +116,11 @@ open class RelayConnectionManager(
}
}
override fun onConnected(relay: IRelayClient, pingMillis: Int, compressed: Boolean) {
override fun onConnected(
relay: IRelayClient,
pingMillis: Int,
compressed: Boolean,
) {
updateRelayStatus(relay.url) {
it.copy(connected = true, pingMs = pingMillis, compressed = compressed, error = null)
}
@@ -124,17 +132,29 @@ open class RelayConnectionManager(
}
}
override fun onCannotConnect(relay: IRelayClient, errorMessage: String) {
override fun onCannotConnect(
relay: IRelayClient,
errorMessage: String,
) {
updateRelayStatus(relay.url) {
it.copy(connected = false, error = errorMessage)
}
}
override fun onIncomingMessage(relay: IRelayClient, msgStr: String, msg: Message) {
override fun onIncomingMessage(
relay: IRelayClient,
msgStr: String,
msg: Message,
) {
// Events are handled by subscription listeners
}
override fun onSent(relay: IRelayClient, cmdStr: String, cmd: Command, success: Boolean) {
override fun onSent(
relay: IRelayClient,
cmdStr: String,
cmd: Command,
success: Boolean,
) {
// Command send tracking
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -38,11 +38,12 @@ data class RelayStatus(
* Default relay URLs for Nostr connectivity.
*/
object DefaultRelays {
val RELAYS = listOf(
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://nostr.wine",
)
val RELAYS =
listOf(
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://nostr.wine",
)
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -63,11 +63,12 @@ fun KeyInputField(
placeholder = { Text(placeholder) },
modifier = modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (showKey) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
visualTransformation =
if (showKey) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
trailingIcon = {
IconButton(onClick = { showKey = !showKey }) {
Icon(
@@ -77,9 +78,10 @@ fun KeyInputField(
}
},
isError = errorMessage != null,
supportingText = errorMessage?.let {
{ Text(it, color = MaterialTheme.colorScheme.error) }
},
supportingText =
errorMessage?.let {
{ Text(it, color = MaterialTheme.colorScheme.error) }
},
)
}
@@ -93,9 +95,10 @@ fun SelectableKeyText(
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,
),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,
),
) {
Text(
text = key,
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -68,9 +68,10 @@ fun LoginCard(
Card(
modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) {
Column(
modifier = Modifier.padding(24.dp),
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -57,9 +57,10 @@ fun NewKeyWarningCard(
) {
Card(
modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
),
) {
Column(
modifier = Modifier.padding(24.dp),
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -47,16 +47,17 @@ fun ProfileInfoCard(
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) {
Column(modifier = Modifier.padding(16.dp)) {
if (isReadOnly) {
Text(
"Read-only mode",
style = MaterialTheme.typography.labelMedium,
color = Color.Yellow
color = Color.Yellow,
)
Spacer(Modifier.height(8.dp))
}
@@ -64,12 +65,12 @@ fun ProfileInfoCard(
Text(
"Public Key",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
npub,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(Modifier.height(16.dp))
@@ -77,12 +78,12 @@ fun ProfileInfoCard(
Text(
"Hex",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
pubKeyHex,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -55,44 +55,46 @@ fun RelayStatusCard(
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
) {
val statusColor = when {
status.connected -> Color.Green
status.error != null -> Color.Red
else -> Color.Gray
}
val statusColor =
when {
status.connected -> Color.Green
status.error != null -> Color.Red
else -> Color.Gray
}
if (status.connected) {
Icon(
Icons.Default.Check,
contentDescription = "Connected",
tint = statusColor,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(20.dp),
)
} else if (status.error != null) {
Icon(
Icons.Default.Close,
contentDescription = "Error",
tint = statusColor,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(20.dp),
)
} else {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
strokeWidth = 2.dp,
)
}
@@ -100,20 +102,20 @@ fun RelayStatusCard(
Text(
status.url.url,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSurface,
)
if (status.connected && status.pingMs != null) {
Text(
"${status.pingMs}ms${if (status.compressed) " • compressed" else ""}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
status.error?.let { error ->
Text(
error,
style = MaterialTheme.typography.bodySmall,
color = Color.Red.copy(alpha = 0.8f)
color = Color.Red.copy(alpha = 0.8f),
)
}
}
@@ -123,7 +125,7 @@ fun RelayStatusCard(
Icon(
Icons.Default.Close,
contentDescription = "Remove relay",
tint = MaterialTheme.colorScheme.onSurfaceVariant
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -29,16 +29,17 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
* Extension to convert Event to NoteDisplayData for the shared NoteCard.
*/
fun Event.toNoteDisplayData(): NoteDisplayData {
val npub = try {
pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..."
} catch (e: Exception) {
pubKey.take(16) + "..."
}
val npub =
try {
pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..."
} catch (e: Exception) {
pubKey.take(16) + "..."
}
return NoteDisplayData(
id = id,
pubKeyDisplay = npub,
content = content,
createdAt = createdAt
createdAt = createdAt,
)
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -29,8 +29,7 @@ fun ByteArray.toHexKey(): HexKey = Hex.encode(this)
fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this)
fun HexKey.hexToByteArrayOrNull(): ByteArray? =
if (Hex.isHex(this)) Hex.decode(this) else null
fun HexKey.hexToByteArrayOrNull(): ByteArray? = if (Hex.isHex(this)) Hex.decode(this) else null
const val PUBKEY_LENGTH = 64