From a34a5e0af73b59c398e1414aace2243610e00d5c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 6 May 2026 13:04:53 +0000 Subject: [PATCH 1/4] fix(profile): hide '_@' prefix on NIP-05 in profile header Per NIP-05, when the local part is '_', the address should display as just the domain. The profile header was calling Nip05Id.toValue() which always returns 'name@domain', producing '_@houseofeff.com' on screen. Match the pattern already used in NIP05VerificationDisplay, AwardBadgeScreen, RelayManagementScreen, and NewCommunityScreen. --- .../ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..c899c25a7 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 @@ -338,8 +338,10 @@ fun DisplayNip05ProfileStatus( Text( text = remember(nip05State) { + val name = nip05State.nip05.name + val display = if (name == "_") nip05State.nip05.domain else "$name@${nip05State.nip05.domain}" buildAnnotatedString { - appendLink(nip05State.nip05.toValue(), color) { + appendLink(display, color) { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } } } From 54c48bc5b7cf951557223a140f8bb84adcf27939 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 6 May 2026 16:28:47 +0200 Subject: [PATCH 2/4] extract Nip05OrPubkeyLine into shared composable Extract nip05 value calculation into helper --- .../loggedIn/badges/award/AwardBadgeScreen.kt | 30 +---------- .../newCommunity/NewCommunityScreen.kt | 30 +---------- .../profile/header/DrawAdditionalInfo.kt | 4 +- .../ui/components/Nip05OrPubkeyLine.kt | 54 +++++++++++++++++++ .../quartz/nip05DnsIdentifiers/Nip05Id.kt | 7 +++ .../quartz/nip05DnsIdentifiers/Nip05Test.kt | 14 +++++ 6 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/Nip05OrPubkeyLine.kt 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 c899c25a7..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 @@ -338,10 +338,8 @@ fun DisplayNip05ProfileStatus( Text( text = remember(nip05State) { - val name = nip05State.nip05.name - val display = if (name == "_") nip05State.nip05.domain else "$name@${nip05State.nip05.domain}" buildAnnotatedString { - appendLink(display, color) { + appendLink(nip05State.nip05.toDisplayValue(), color) { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } } } 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..87e8157d4 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/Nip05OrPubkeyLine.kt @@ -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, + ) +} 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..f329a18c5 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,13 @@ 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) + 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..28996c662 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,20 @@ 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 `test json parsing with relays`() { val parsedNip05 = Nip05Id.parse("bob@test.com") From d2e04a6726eace4ecbc4f6d45a4b1b73fa8bf70d Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 6 May 2026 17:08:50 +0200 Subject: [PATCH 3/4] Code review: exhaustive when on Nip05State in Nip05OrPubkeyLine memoize pubkey display in Nip05OrPubkeyLine --- .../commons/ui/components/Nip05OrPubkeyLine.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 index 87e8157d4..73c69da9c 100644 --- 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 @@ -24,24 +24,22 @@ 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 -/** - * 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. - */ +/** 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() - else -> user.pubkeyDisplayHex() + is Nip05State.NotFound -> pubkeyShort } Text( From fe2cb7da662cdaf4f687d49764723529e7a14edc Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 6 May 2026 17:28:36 +0200 Subject: [PATCH 4/4] Centralise the NIP-05 wildcard check (`name != "_"`) on the data class itself. --- .../amethyst/ui/note/NIP05VerificationDisplay.kt | 4 ++-- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../loggedIn/relays/nip86/RelayManagementScreen.kt | 2 +- .../quartz/nip05DnsIdentifiers/Nip05Id.kt | 6 ++++++ .../quartz/nip05DnsIdentifiers/Nip05Test.kt | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 4 deletions(-) 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/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/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt index f329a18c5..537210e8f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt @@ -36,6 +36,12 @@ data class Nip05Id( */ 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 28996c662..238522513 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt @@ -123,6 +123,20 @@ class Nip05Test { 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")