extract Nip05OrPubkeyLine into shared composable

Extract nip05 value calculation into helper
This commit is contained in:
davotoula
2026-05-06 16:28:47 +02:00
parent a34a5e0af7
commit 54c48bc5b7
6 changed files with 80 additions and 59 deletions
@@ -48,10 +48,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
@@ -231,35 +230,10 @@ private fun SelectedUserRow(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
UserSecondaryLine(user) Nip05OrPubkeyLine(user)
} }
TextButton(onClick = onClear) { TextButton(onClick = onClear) {
Text(stringRes(R.string.award_badge_remove_recipient)) Text(stringRes(R.string.award_badge_remove_recipient))
} }
} }
} }
@Composable
private fun UserSecondaryLine(user: User) {
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
val text =
when (val state = nip05StateMetadata) {
is Nip05State.Exists -> {
val name = state.nip05.name
if (name == "_") state.nip05.domain else "$name@${state.nip05.domain}"
}
else -> {
user.pubkeyDisplayHex()
}
}
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@@ -65,14 +65,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
@@ -497,7 +496,7 @@ private fun SelectedModeratorRow(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
ModeratorSecondaryLine(user) Nip05OrPubkeyLine(user)
} }
if (isOwner) { if (isOwner) {
@@ -522,31 +521,6 @@ private fun SelectedModeratorRow(
} }
} }
@Composable
private fun ModeratorSecondaryLine(user: User) {
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
val text =
when (val state = nip05StateMetadata) {
is Nip05State.Exists -> {
val name = state.nip05.name
if (name == "_") state.nip05.domain else "$name@${state.nip05.domain}"
}
else -> {
user.pubkeyDisplayHex()
}
}
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
// --- Relays ------------------------------------------------------------------------------------ // --- Relays ------------------------------------------------------------------------------------
@Composable @Composable
@@ -338,10 +338,8 @@ fun DisplayNip05ProfileStatus(
Text( Text(
text = text =
remember(nip05State) { remember(nip05State) {
val name = nip05State.nip05.name
val display = if (name == "_") nip05State.nip05.domain else "$name@${nip05State.nip05.domain}"
buildAnnotatedString { buildAnnotatedString {
appendLink(display, color) { appendLink(nip05State.nip05.toDisplayValue(), color) {
runCatching { uri.openUri("https://${nip05State.nip05.domain}") } runCatching { uri.openUri("https://${nip05State.nip05.domain}") }
} }
} }
@@ -0,0 +1,54 @@
/*
* 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
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
/**
* Single-line bodySmall label for a user secondary identifier: shows the
* verified NIP-05 (formatted via [com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id.toDisplayValue])
* when present, otherwise a shortened pubkey.
*/
@Composable
fun Nip05OrPubkeyLine(user: User) {
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
val text =
when (val state = nip05StateMetadata) {
is Nip05State.Exists -> state.nip05.toDisplayValue()
else -> user.pubkeyDisplayHex()
}
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@@ -29,6 +29,13 @@ data class Nip05Id(
) { ) {
fun toValue(): String = assemble(name, domain) fun toValue(): String = assemble(name, domain)
/**
* Renders the address for users to read. When [name] is `"_"`, NIP-05
* specifies the address should display as just the domain. Use this in UI;
* use [toValue] for network lookups and storage.
*/
fun toDisplayValue(): String = if (name == "_") domain else assemble(name, domain)
fun toUserUrl(): String = userUrl(name, domain) fun toUserUrl(): String = userUrl(name, domain)
fun toDomainUrl(): String = domainUrl(domain) fun toDomainUrl(): String = domainUrl(domain)
@@ -109,6 +109,20 @@ class Nip05Test {
assertEquals("https://$domain/.well-known/nostr.json", parsedNip05.toDomainUrl()) assertEquals("https://$domain/.well-known/nostr.json", parsedNip05.toDomainUrl())
} }
@Test
fun `toDisplayValue with regular name returns name@domain`() {
val nip05 = Nip05Id.parse("alice@example.com")
assertNotNull(nip05)
assertEquals("alice@example.com", nip05.toDisplayValue())
}
@Test
fun `toDisplayValue with underscore name returns domain only`() {
val nip05 = Nip05Id.parse("_@example.com")
assertNotNull(nip05)
assertEquals("example.com", nip05.toDisplayValue())
}
@Test @Test
fun `test json parsing with relays`() { fun `test json parsing with relays`() {
val parsedNip05 = Nip05Id.parse("bob@test.com") val parsedNip05 = Nip05Id.parse("bob@test.com")