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 =
listOf(
AppScreen.Feed, AppScreen.Feed,
AppScreen.Search, AppScreen.Search,
AppScreen.Messages, AppScreen.Messages,
AppScreen.Notifications, AppScreen.Notifications,
AppScreen.Profile, AppScreen.Profile,
) )
/** /**
* Secondary navigation destinations (settings, etc.) * Secondary navigation destinations (settings, etc.)
*/ */
val secondaryScreens = listOf( val secondaryScreens =
listOf(
AppScreen.Settings, 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,9 +83,10 @@ 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 =
when {
connectedCount == 0 -> RelayStatusColors.Disconnected connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected else -> RelayStatusColors.Connected
@@ -96,20 +96,20 @@ fun RelayStatusIndicator(
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,7 +103,8 @@ open class RelayConnectionManager(
url: NormalizedRelayUrl, url: NormalizedRelayUrl,
update: (RelayStatus) -> RelayStatus, update: (RelayStatus) -> RelayStatus,
) { ) {
_relayStatuses.value = _relayStatuses.value.toMutableMap().apply { _relayStatuses.value =
_relayStatuses.value.toMutableMap().apply {
val current = this[url] ?: RelayStatus(url, connected = false) val current = this[url] ?: RelayStatus(url, connected = false)
this[url] = update(current) this[url] = update(current)
} }
@@ -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,7 +38,8 @@ data class RelayStatus(
* Default relay URLs for Nostr connectivity. * Default relay URLs for Nostr connectivity.
*/ */
object DefaultRelays { object DefaultRelays {
val RELAYS = listOf( val RELAYS =
listOf(
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.nostr.band", "wss://relay.nostr.band",
"wss://nos.lol", "wss://nos.lol",
@@ -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,7 +63,8 @@ fun KeyInputField(
placeholder = { Text(placeholder) }, placeholder = { Text(placeholder) },
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = if (showKey) { visualTransformation =
if (showKey) {
VisualTransformation.None VisualTransformation.None
} else { } else {
PasswordVisualTransformation() PasswordVisualTransformation()
@@ -77,7 +78,8 @@ fun KeyInputField(
} }
}, },
isError = errorMessage != null, isError = errorMessage != null,
supportingText = errorMessage?.let { supportingText =
errorMessage?.let {
{ Text(it, color = MaterialTheme.colorScheme.error) } { Text(it, color = MaterialTheme.colorScheme.error) }
}, },
) )
@@ -93,7 +95,8 @@ fun SelectableKeyText(
) { ) {
Card( Card(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors( colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
), ),
) { ) {
@@ -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,7 +68,8 @@ fun LoginCard(
Card( Card(
modifier = modifier.width(cardWidth), modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors( colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
), ),
) { ) {
@@ -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,7 +57,8 @@ fun NewKeyWarningCard(
) { ) {
Card( Card(
modifier = modifier.width(cardWidth), modifier = modifier.width(cardWidth),
colors = CardDefaults.cardColors( colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
), ),
) { ) {
@@ -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,21 +55,23 @@ 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 =
when {
status.connected -> Color.Green status.connected -> Color.Green
status.error != null -> Color.Red status.error != null -> Color.Red
else -> Color.Gray else -> Color.Gray
@@ -80,19 +82,19 @@ fun RelayStatusCard(
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,7 +29,8 @@ 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 =
try {
pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..." pubKey.hexToByteArrayOrNull()?.toNpub() ?: pubKey.take(16) + "..."
} catch (e: Exception) { } catch (e: Exception) {
pubKey.take(16) + "..." pubKey.take(16) + "..."
@@ -39,6 +40,6 @@ fun Event.toNoteDisplayData(): 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