diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 8004ccdde..e712185e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -32,8 +32,11 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.namecoin.NamecoinNameService import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.quartz.nip05.namecoin.NamecoinNameResolver import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -43,7 +46,9 @@ import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -69,12 +74,41 @@ class SearchBarViewModel( val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account) + /** + * Resolves Namecoin identifiers (.bit / d/ / id/) via ElectrumX and + * returns the matching [User] from LocalCache, or null. + */ + private val namecoinResolvedUser = + searchValueFlow + .debounce(400) + .distinctUntilChanged() + .filter { NamecoinNameResolver.isNamecoinIdentifier(it) } + .map { term -> + try { + val result = NamecoinNameService.getInstance().resolve(term) + if (result != null) { + LocalCache.getOrCreateUser(result.pubkey) + } else { + null + } + } catch (_: Exception) { + null + } + }.flowOn(Dispatchers.IO) + .stateIn(viewModelScope, WhileSubscribed(5000), null) + val searchResultsUsers = combine( searchValueFlow.debounce(100), invalidations.debounce(100), - ) { term, version -> - LocalCache.findUsersStartingWith(term, account) + namecoinResolvedUser, + ) { term, version, namecoinUser -> + val localResults = LocalCache.findUsersStartingWith(term, account) + if (namecoinUser != null && localResults.none { it.pubkeyHex == namecoinUser.pubkeyHex }) { + listOf(namecoinUser) + localResults + } else { + localResults + } }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) diff --git a/docs/namecoin-nip05-design.md b/docs/namecoin-nip05-design.md index 468c9f5f1..8289ac9d2 100644 --- a/docs/namecoin-nip05-design.md +++ b/docs/namecoin-nip05-design.md @@ -154,6 +154,17 @@ The integration is minimal and non-invasive: 3. Non-Namecoin identifiers are completely unaffected 4. **`AppModules`** wires up the resolver at construction time +## Search Integration + +The search bar resolves Namecoin identifiers in real-time via `SearchBarViewModel`: + +1. A `namecoinResolvedUser` flow watches the search input with a 400ms debounce +2. If the input matches any Namecoin format (`d/*`, `id/*`, `*.bit`, `*@*.bit`), it resolves via `NamecoinNameService` → `ElectrumxClient` → blockchain +3. The resolved pubkey is used to get/create a `User` in `LocalCache` +4. The Namecoin-resolved user is prepended to the standard local search results (deduplicated) + +This means typing `alice@example.bit`, `example.bit`, `d/example`, or `id/alice` into the search bar will query the Namecoin blockchain and show the resolved user profile at the top of results. + ## Default ElectrumX Server ``` @@ -202,6 +213,7 @@ Search bar integration. When a user types a `.bit` identifier, shows a loading s ### Modified files - `amethyst/.../AppModules.kt` — Wire up `NamecoinNameResolver` into `Nip05Client` +- `amethyst/.../ui/screen/loggedIn/search/SearchBarViewModel.kt` — Namecoin search resolution - `quartz/.../nip05DnsIdentifiers/Nip05Client.kt` — Route `.bit` identifiers to Namecoin resolver - `amethyst/.../relays/RelayInformationScreen.kt` — Import reordering (spotless) @@ -212,13 +224,34 @@ Search bar integration. When a user types a `.bit` identifier, shows a loading s ./gradlew :quartz:jvmTest --tests "*NamecoinNameResolverTest*" ``` -### Manual testing (emulator) -1. Build and install: `./gradlew :amethyst:installFdroidDebug` -2. Search for `m@testls.bit` — should resolve to pubkey `6cdebcca...18667d` -3. Search for `testls.bit` — root domain lookup -4. Search for `d/testls` — direct namespace format +### Manual testing (emulator or device) -### Live verification +Build and install the debug APK: +```bash +./gradlew assemblePlayDebug +adb install -r amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk +``` + +**Search bar tests** — open the search bar and enter each of these: + +| Search query | Expected result | What it tests | +|---|---|---| +| `m@testls.bit` | Resolves to Vitor Pamplona's profile | NIP-05 style `user@domain.bit` | +| `testls.bit` | Resolves to Vitor Pamplona's profile (root `_` entry) | Bare domain `.bit` lookup | +| `d/testls` | Resolves to Vitor Pamplona's profile | Direct `d/` namespace | +| `id/someuser` | Resolves if registered on-chain | Direct `id/` namespace | + +**Verification test** — if a profile has a `.bit` address in its `nip05` field, the NIP-05 badge should verify via the blockchain instead of HTTP. + +**Network verification** — to confirm ElectrumX calls are being made: +```bash +# Monitor traffic to ElectrumX ports on the emulator +adb root +adb shell tcpdump -i any -nn port 50002 or port 50006 +``` +You should see TCP connections to `162.212.154.52:50002` (electrumx.testls.space) when searching for `.bit` identifiers. + +### Live test data The name `d/testls` is registered on the Namecoin blockchain (block 551519+, last updated block 814278) with value: ```json { @@ -229,3 +262,8 @@ The name `d/testls` is registered on the Namecoin blockchain (block 551519+, las } } ``` + +This means: +- `m@testls.bit` → resolves `m` entry → pubkey `6cdebcca...18667d` +- `testls.bit` → resolves root `_` entry → falls back to first available entry +- `d/testls` → same as `testls.bit` (root lookup)