feat(search): inline Namecoin resolution indicator in global search bar

Reuses the NamecoinResolutionRow composable already shipping for the
onchain-zap recipient field, promoting it from
ui/screen/loggedIn/wallet/ to a generic ui/components/namecoin/
location so it can be mounted anywhere a .bit-shaped search input may
race the local-cache prefix search.

In the global search bar, typing a bare ".bit" host (e.g.
"testls.bit") used to surface a cached sibling profile like
"m@testls.bit" first (LocalCache.findUsersStartingWith hits the
prefix) and only several seconds later be corrected by the slower
on-chain ElectrumX resolution from
SearchBarViewModel.directNip05Resolver. No in-flight indicator and no
feedback on hard failures (timeout, malformed record, etc.).

Changes:

  - git-rename NamecoinResolutionRow.kt and its test from
    ui/screen/loggedIn/wallet/ to ui/components/namecoin/, updating
    the package declaration only.
  - Add an optional `modifier: Modifier = Modifier` parameter to the
    composable (standard Compose convention) and wrap the spinner /
    result / error rows in a Column taking the caller-provided
    modifier. No visual change in OnchainZapSendDialog.
  - Update OnchainZapSendDialog import to the new package location.
  - Mount NamecoinResolutionRow in SearchScreen.SearchBar between
    SearchTextField and SearchFilterRow, with horizontal padding to
    match the rest of the bar. onUserResolved navigates to the user
    and clears the field, matching the bech32 auto-resolve path in
    SearchBarViewModel.directRouteResolver.

State is held in the shared
commons.NamecoinResolveState (no new state class introduced) and
diagnostic wording comes from the existing mapOutcomeToResolveState
helper, so every Namecoin surface continues to produce the same
message for the same outcome.
This commit is contained in:
m
2026-05-19 06:33:47 +10:00
parent 34cb4eb2ad
commit 1c5230cfc5
4 changed files with 39 additions and 12 deletions
@@ -18,7 +18,7 @@
* 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.ui.screen.loggedIn.wallet
package com.vitorpamplona.amethyst.ui.components.namecoin
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -111,15 +111,16 @@ fun looksLikeNamecoinIdentifier(raw: String): Boolean {
}
/**
* Inline Namecoin resolution indicator + result row, sandwiched between
* the recipient text field and the local-cache suggestion dropdown in
* [OnchainZapSendDialog].
* Inline Namecoin resolution indicator + result row. Designed to be
* mounted alongside any text input whose local-cache prefix search can
* race ahead of an on-chain `.bit` lookup (the onchain-zap recipient
* field and the global search bar both have this race).
*
* Behaviour:
* - Renders nothing when [searchInput] is not a `.bit` identifier.
* - Shows a small spinner row ("Resolving on Namecoin…") while the
* ElectrumX lookup is in flight (after a 300 ms debounce to match
* the dropdown's own debounce).
* typical input-field debounce intervals).
* - On success, shows the resolved user as a tappable row with a
* `MaterialSymbols.Link` badge labelled "Namecoin"; tapping calls
* [onUserResolved].
@@ -132,12 +133,16 @@ fun looksLikeNamecoinIdentifier(raw: String): Boolean {
* The composable is intentionally self-contained: it owns its own
* [LaunchedEffect] keyed on [searchInput], so it cancels in-flight
* lookups whenever the user keeps typing.
*
* @param modifier applied to the outer `Column` so callers can position
* or pad the row (e.g. the search bar pads horizontally).
*/
@Composable
fun NamecoinResolutionRow(
searchInput: String,
accountViewModel: AccountViewModel,
onUserResolved: (User) -> Unit,
modifier: Modifier = Modifier,
) {
val trimmed = remember(searchInput) { searchInput.trim().removePrefix("@") }
if (!looksLikeNamecoinIdentifier(trimmed)) return
@@ -166,12 +171,14 @@ fun NamecoinResolutionRow(
}
}
Spacer(Modifier.size(8.dp))
when (val s = state) {
null, NamecoinResolveState.Loading -> ResolvingChip(trimmed)
is NamecoinResolveState.Resolved -> ResolvedRow(trimmed, s, accountViewModel, onUserResolved)
NamecoinResolveState.NotFound -> FailedRow("No record for $trimmed on Namecoin.")
is NamecoinResolveState.Error -> FailedRow(s.message)
Column(modifier = modifier) {
Spacer(Modifier.size(8.dp))
when (val s = state) {
null, NamecoinResolveState.Loading -> ResolvingChip(trimmed)
is NamecoinResolveState.Resolved -> ResolvedRow(trimmed, s, accountViewModel, onUserResolved)
NamecoinResolveState.NotFound -> FailedRow("No record for $trimmed on Namecoin.")
is NamecoinResolveState.Error -> FailedRow(s.message)
}
}
}
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.search.SearchSource
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
@@ -194,6 +195,24 @@ private fun SearchBar(
Column(modifier = Modifier.statusBarsPadding()) {
SearchTextField(searchBarViewModel, Modifier)
// Inline Namecoin lookup feedback for the global search field.
// Mirrors the wiring in OnchainZapSendDialog: the local prefix
// search can race ahead of the on-chain resolution and show a
// cached sibling profile (e.g. "m@testls.bit") before the bare
// ".bit" host resolves to its `_@host` profile. Surfaces the
// in-flight state, the eventual on-chain match, and any failure
// explicitly. Tapping the resolved row navigates to the user and
// clears the search field, matching the existing bech32 auto-
// resolve behaviour in `SearchBarViewModel.directRouteResolver`.
NamecoinResolutionRow(
searchInput = searchBarViewModel.searchValue,
accountViewModel = accountViewModel,
onUserResolved = { user ->
nav.nav(routeFor(user))
searchBarViewModel.clear()
},
modifier = Modifier.padding(horizontal = 10.dp),
)
SearchFilterRow(searchBarViewModel)
}
}
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
@@ -18,7 +18,7 @@
* 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.ui.screen.loggedIn.wallet
package com.vitorpamplona.amethyst.ui.components.namecoin
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinResolveState
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNostrResult