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.key.Keyer
import coil3.request.Options import coil3.request.Options
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image 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.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.sha256.sha256 import com.vitorpamplona.quartz.utils.sha256.sha256
@@ -42,7 +43,7 @@ class Base64Fetcher(
override suspend fun fetch(): FetchResult? = override suspend fun fetch(): FetchResult? =
runCatching { runCatching {
ImageFetchResult( ImageFetchResult(
image = Base64Image.Companion.toBitmap(data.toString()).asImage(true), image = Base64Image.toBitmap(data.toString()).asImage(true),
isSampled = false, isSampled = false,
dataSource = DataSource.MEMORY, dataSource = DataSource.MEMORY,
) )
@@ -30,6 +30,7 @@ import coil3.fetch.ImageFetchResult
import coil3.key.Keyer import coil3.key.Keyer
import coil3.request.Options import coil3.request.Options
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
import com.vitorpamplona.amethyst.commons.blurhash.toAndroidBitmap
data class BlurhashWrapper( data class BlurhashWrapper(
val blurhash: String, val blurhash: String,
@@ -43,10 +44,10 @@ class BlurHashFetcher(
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val hash = data.blurhash val hash = data.blurhash
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null val platformImage = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: return null
return ImageFetchResult( return ImageFetchResult(
image = bitmap.asImage(true), image = platformImage.toAndroidBitmap().asImage(true),
isSampled = false, isSampled = false,
dataSource = DataSource.MEMORY, 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 * 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 * 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). * Primary navigation destinations (shown in bottom bar on mobile, sidebar on desktop).
*/ */
val primaryScreens = listOf( val primaryScreens =
AppScreen.Feed, listOf(
AppScreen.Search, AppScreen.Feed,
AppScreen.Messages, AppScreen.Search,
AppScreen.Notifications, AppScreen.Messages,
AppScreen.Profile, AppScreen.Notifications,
) AppScreen.Profile,
)
/** /**
* Secondary navigation destinations (settings, etc.) * Secondary navigation destinations (settings, etc.)
*/ */
val secondaryScreens = listOf( val secondaryScreens =
AppScreen.Settings, 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 * 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 * 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 * 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 * 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 * 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 * 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.runtime.Composable
import androidx.compose.ui.Alignment 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.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.theme.RelayStatusColors import com.vitorpamplona.amethyst.commons.ui.theme.RelayStatusColors
@@ -57,12 +56,12 @@ fun FeedHeader(
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
title, title,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground,
) )
RelayStatusIndicator( RelayStatusIndicator(
@@ -84,32 +83,33 @@ fun RelayStatusIndicator(
Row( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
val statusColor = when { val statusColor =
connectedCount == 0 -> RelayStatusColors.Disconnected when {
connectedCount < 3 -> RelayStatusColors.Connecting connectedCount == 0 -> RelayStatusColors.Disconnected
else -> RelayStatusColors.Connected connectedCount < 3 -> RelayStatusColors.Connecting
} else -> RelayStatusColors.Connected
}
Icon( Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close, imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
contentDescription = null, contentDescription = null,
tint = statusColor, tint = statusColor,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp),
) )
Text( Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}", "$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall,
) )
IconButton(onClick = onRefresh) { IconButton(onClick = onRefresh) {
Icon( Icon(
Icons.Default.Refresh, Icons.Default.Refresh,
contentDescription = "Reconnect", 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -42,12 +42,12 @@ fun PlaceholderScreen(
Text( Text(
title, title,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground,
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
description, 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 * 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 * 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 * 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 * 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). * Formats a count to human-readable format with suffix (K, M, G).
* Example: 1500 -> "1K items", 2500000 -> "2M items" * Example: 1500 -> "1K items", 2500000 -> "2M items"
*/ */
fun countToHumanReadable(counter: Int, suffix: String): String = fun countToHumanReadable(
counter: Int,
suffix: String,
): String =
when { when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix" counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $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). * 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 { when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix" counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $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 * 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 * 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -92,7 +92,10 @@ open class RelayConnectionManager(
client.close(subId) 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) client.send(event, relays)
} }
@@ -100,10 +103,11 @@ open class RelayConnectionManager(
url: NormalizedRelayUrl, url: NormalizedRelayUrl,
update: (RelayStatus) -> RelayStatus, update: (RelayStatus) -> RelayStatus,
) { ) {
_relayStatuses.value = _relayStatuses.value.toMutableMap().apply { _relayStatuses.value =
val current = this[url] ?: RelayStatus(url, connected = false) _relayStatuses.value.toMutableMap().apply {
this[url] = update(current) val current = this[url] ?: RelayStatus(url, connected = false)
} this[url] = update(current)
}
} }
override fun onConnecting(relay: IRelayClient) { 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) { updateRelayStatus(relay.url) {
it.copy(connected = true, pingMs = pingMillis, compressed = compressed, error = null) 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) { updateRelayStatus(relay.url) {
it.copy(connected = false, error = errorMessage) 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 // 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 // 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -38,11 +38,12 @@ data class RelayStatus(
* Default relay URLs for Nostr connectivity. * Default relay URLs for Nostr connectivity.
*/ */
object DefaultRelays { object DefaultRelays {
val RELAYS = listOf( val RELAYS =
"wss://relay.damus.io", listOf(
"wss://relay.nostr.band", "wss://relay.damus.io",
"wss://nos.lol", "wss://relay.nostr.band",
"wss://relay.snort.social", "wss://nos.lol",
"wss://nostr.wine", "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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -63,11 +63,12 @@ fun KeyInputField(
placeholder = { Text(placeholder) }, placeholder = { Text(placeholder) },
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = if (showKey) { visualTransformation =
VisualTransformation.None if (showKey) {
} else { VisualTransformation.None
PasswordVisualTransformation() } else {
}, PasswordVisualTransformation()
},
trailingIcon = { trailingIcon = {
IconButton(onClick = { showKey = !showKey }) { IconButton(onClick = { showKey = !showKey }) {
Icon( Icon(
@@ -77,9 +78,10 @@ fun KeyInputField(
} }
}, },
isError = errorMessage != null, isError = errorMessage != null,
supportingText = errorMessage?.let { supportingText =
{ Text(it, color = MaterialTheme.colorScheme.error) } errorMessage?.let {
}, { Text(it, color = MaterialTheme.colorScheme.error) }
},
) )
} }
@@ -93,9 +95,10 @@ fun SelectableKeyText(
) { ) {
Card( Card(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors( colors =
containerColor = MaterialTheme.colorScheme.surface, CardDefaults.cardColors(
), containerColor = MaterialTheme.colorScheme.surface,
),
) { ) {
Text( Text(
text = key, 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -68,9 +68,10 @@ fun LoginCard(
Card( Card(
modifier = modifier.width(cardWidth), modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors( colors =
containerColor = MaterialTheme.colorScheme.surfaceVariant, CardDefaults.cardColors(
), containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) { ) {
Column( Column(
modifier = Modifier.padding(24.dp), 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -57,9 +57,10 @@ fun NewKeyWarningCard(
) { ) {
Card( Card(
modifier = modifier.width(cardWidth), modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors( colors =
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), CardDefaults.cardColors(
), containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
),
) { ) {
Column( Column(
modifier = Modifier.padding(24.dp), 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -47,16 +47,17 @@ fun ProfileInfoCard(
) { ) {
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors =
containerColor = MaterialTheme.colorScheme.surfaceVariant CardDefaults.cardColors(
) containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
if (isReadOnly) { if (isReadOnly) {
Text( Text(
"Read-only mode", "Read-only mode",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color.Yellow color = Color.Yellow,
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
} }
@@ -64,12 +65,12 @@ fun ProfileInfoCard(
Text( Text(
"Public Key", "Public Key",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Text( Text(
npub, npub,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@@ -77,12 +78,12 @@ fun ProfileInfoCard(
Text( Text(
"Hex", "Hex",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Text( Text(
pubKeyHex, pubKeyHex,
style = MaterialTheme.typography.bodySmall, 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 * 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 * this software and associated documentation files (the "Software"), to deal in
@@ -55,44 +55,46 @@ fun RelayStatusCard(
) { ) {
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors =
containerColor = MaterialTheme.colorScheme.surfaceVariant CardDefaults.cardColors(
) containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(12.dp), modifier = Modifier.fillMaxWidth().padding(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) { ) {
val statusColor = when { val statusColor =
status.connected -> Color.Green when {
status.error != null -> Color.Red status.connected -> Color.Green
else -> Color.Gray status.error != null -> Color.Red
} else -> Color.Gray
}
if (status.connected) { if (status.connected) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Connected", contentDescription = "Connected",
tint = statusColor, tint = statusColor,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp),
) )
} else if (status.error != null) { } else if (status.error != null) {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,
contentDescription = "Error", contentDescription = "Error",
tint = statusColor, tint = statusColor,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp),
) )
} else { } else {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
strokeWidth = 2.dp strokeWidth = 2.dp,
) )
} }
@@ -100,20 +102,20 @@ fun RelayStatusCard(
Text( Text(
status.url.url, status.url.url,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
if (status.connected && status.pingMs != null) { if (status.connected && status.pingMs != null) {
Text( Text(
"${status.pingMs}ms${if (status.compressed) " • compressed" else ""}", "${status.pingMs}ms${if (status.compressed) " • compressed" else ""}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
status.error?.let { error -> status.error?.let { error ->
Text( Text(
error, error,
style = MaterialTheme.typography.bodySmall, 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( Icon(
Icons.Default.Close, Icons.Default.Close,
contentDescription = "Remove relay", 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 * 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 * 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. * Extension to convert Event to NoteDisplayData for the shared NoteCard.
*/ */
fun Event.toNoteDisplayData(): NoteDisplayData { fun Event.toNoteDisplayData(): NoteDisplayData {
val npub = try { val npub =
pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..." try {
} catch (e: Exception) { pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..."
pubKey.take(16) + "..." } catch (e: Exception) {
} pubKey.take(16) + "..."
}
return NoteDisplayData( return NoteDisplayData(
id = id, id = id,
pubKeyDisplay = npub, pubKeyDisplay = npub,
content = content, 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 * 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 * 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 * 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 * 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.hexToByteArray(): ByteArray = Hex.decode(this)
fun HexKey.hexToByteArrayOrNull(): ByteArray? = fun HexKey.hexToByteArrayOrNull(): ByteArray? = if (Hex.isHex(this)) Hex.decode(this) else null
if (Hex.isHex(this)) Hex.decode(this) else null
const val PUBKEY_LENGTH = 64 const val PUBKEY_LENGTH = 64