diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 817fd27d0..b33793d13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -378,7 +378,7 @@ fun ObserveAndDisplayNIP05( ) { val uri = LocalUriHandler.current - if (nip05State.nip05.name != "_") { + if (nip05State.nip05.hasLocalPart()) { Text( text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, fontSize = Font14SP, @@ -407,7 +407,7 @@ fun DisplayNIP05( ) { val uri = LocalUriHandler.current - if (nip05State.nip05.name != "_") { + if (nip05State.nip05.hasLocalPart()) { Text( text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, fontSize = Font14SP, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index 1dbf06030..cfa7304b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -188,7 +188,7 @@ private fun NonClickableObserveAndDisplayNIP05( nip05State: Nip05State.Exists, accountViewModel: AccountViewModel, ) { - if (nip05State.nip05.name != "_") { + if (nip05State.nip05.hasLocalPart()) { Text( text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, fontSize = Font14SP, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt index f600bfd52..2172e1894 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt @@ -48,10 +48,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel 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.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar @@ -231,35 +230,10 @@ private fun SelectedUserRow( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - UserSecondaryLine(user) + Nip05OrPubkeyLine(user) } TextButton(onClick = onClear) { 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, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index d6b3698c5..f8100fc1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt @@ -65,14 +65,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon 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.User import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog @@ -497,7 +496,7 @@ private fun SelectedModeratorRow( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - ModeratorSecondaryLine(user) + Nip05OrPubkeyLine(user) } 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 ------------------------------------------------------------------------------------ @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 8a382fb66..ea12f1bf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -339,7 +339,7 @@ fun DisplayNip05ProfileStatus( text = remember(nip05State) { buildAnnotatedString { - appendLink(nip05State.nip05.toValue(), color) { + appendLink(nip05State.nip05.toDisplayValue(), color) { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index 531a9d5c3..79a87fffb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -490,7 +490,7 @@ private fun PubkeyNip05Row( when (val nip05State = nip05StateMetadata) { is Nip05State.Exists -> { - if (nip05State.nip05.name != "_") { + if (nip05State.nip05.hasLocalPart()) { Text( text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, fontSize = Font14SP, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/Nip05OrPubkeyLine.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/Nip05OrPubkeyLine.kt new file mode 100644 index 000000000..73c69da9c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/Nip05OrPubkeyLine.kt @@ -0,0 +1,52 @@ +/* + * 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.runtime.remember +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 + +/** Shows the verified NIP-05 if present, otherwise a shortened pubkey. */ +@Composable +fun Nip05OrPubkeyLine(user: User) { + val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle() + val pubkeyShort = remember(user.pubkeyHex) { user.pubkeyDisplayHex() } + + val text = + when (val state = nip05StateMetadata) { + is Nip05State.Exists -> state.nip05.toDisplayValue() + is Nip05State.NotFound -> pubkeyShort + } + + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt index c2c543d8b..537210e8f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt @@ -29,6 +29,19 @@ data class Nip05Id( ) { 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) + + /** + * False when [name] is the NIP-05 wildcard `"_"`; use to gate rendering of + * the local part when name and domain are shown as separate widgets. + */ + fun hasLocalPart(): Boolean = name != "_" + fun toUserUrl(): String = userUrl(name, domain) fun toDomainUrl(): String = domainUrl(domain) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt index 630be2bdb..238522513 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt @@ -109,6 +109,34 @@ class Nip05Test { 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 + fun `hasLocalPart is true for regular name`() { + val nip05 = Nip05Id.parse("alice@example.com") + assertNotNull(nip05) + assertEquals(true, nip05.hasLocalPart()) + } + + @Test + fun `hasLocalPart is false for underscore name`() { + val nip05 = Nip05Id.parse("_@example.com") + assertNotNull(nip05) + assertEquals(false, nip05.hasLocalPart()) + } + @Test fun `test json parsing with relays`() { val parsedNip05 = Nip05Id.parse("bob@test.com")