refactor: simplify .bit search resolution per review feedback
Address @vitorpamplona's review comments on the last commit:
1. Remove NamecoinResolutionState sealed class and status banners
— the existing NIP-05 resolver already handles .bit, no need for
separate Namecoin-specific UI state (spinner, chain emoji, error)
2. Remove separate toNip05IdOrNull() resolution path
— widen the existing `if (term.contains('@'))` gate in both
SearchBarViewModel and UserSuggestionState to also accept bare
.bit domains (synthesize _@domain.bit → existing NIP-05 flow)
3. Remove d/ and id/ identifier support from search
— per feedback, only NIP-05 style resolution (.bit) in search
4. Remove special Namecoin result rendering in ImportFollowList
— just show UserLine for all results, no separation needed
5. Keep a lightweight 'Namecoin' label on resolved users in search
— namecoinResolvedUser StateFlow tracks the .bit-resolved user,
shown as a small label above the UserCompose row
Net: -405 lines, +50 lines across 6 files.
This commit is contained in:
+1
-59
@@ -30,33 +30,23 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
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.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
|
||||||
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.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
|
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.WatchAndDisplayNip05Row
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -155,51 +145,3 @@ fun UserLine(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WatchAndDisplayNip05Row(
|
|
||||||
user: User,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
when (val nip05State = nip05StateMetadata) {
|
|
||||||
is Nip05State.Exists -> {
|
|
||||||
NonClickableObserveAndDisplayNIP05(nip05State, accountViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Text(
|
|
||||||
text = user.pubkeyDisplayHex(),
|
|
||||||
fontSize = Font14SP,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NonClickableObserveAndDisplayNIP05(
|
|
||||||
nip05State: Nip05State.Exists,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
if (nip05State.nip05.name != "_") {
|
|
||||||
Text(
|
|
||||||
text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) },
|
|
||||||
fontSize = Font14SP,
|
|
||||||
color = MaterialTheme.colorScheme.nip05,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ObserveAndRenderNIP05VerifiedSymbol(nip05State, 1, NIP05IconSize, accountViewModel)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = nip05State.nip05.domain,
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Visible,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-67
@@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||||
@@ -58,44 +57,6 @@ val userUriPrefixes =
|
|||||||
DualCase("nostr:nprofile"),
|
DualCase("nostr:nprofile"),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** UI state for Namecoin resolution progress. */
|
|
||||||
sealed class NamecoinResolutionState {
|
|
||||||
data object Idle : NamecoinResolutionState()
|
|
||||||
|
|
||||||
data object Resolving : NamecoinResolutionState()
|
|
||||||
|
|
||||||
data class Resolved(
|
|
||||||
val user: User,
|
|
||||||
val namecoinName: String,
|
|
||||||
) : NamecoinResolutionState()
|
|
||||||
|
|
||||||
data class Error(
|
|
||||||
val message: String,
|
|
||||||
) : NamecoinResolutionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a [Nip05Id] for identifiers that should go through NIP-05 / Namecoin resolution. */
|
|
||||||
private fun toNip05IdOrNull(prefix: String): Nip05Id? =
|
|
||||||
when {
|
|
||||||
prefix.contains('@') -> {
|
|
||||||
Nip05Id.parse(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
NamecoinNameResolver.isNamecoinIdentifier(prefix) -> {
|
|
||||||
if (prefix.endsWith(".bit", ignoreCase = true)) {
|
|
||||||
// Bare .bit domain → synthesize _@domain.bit so Nip05Client routes to Namecoin
|
|
||||||
Nip05Id("_", prefix.lowercase())
|
|
||||||
} else {
|
|
||||||
// d/ or id/ — wrap as NIP-05 so it reaches the resolver
|
|
||||||
Nip05Id("_", prefix.lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class UserSuggestionState(
|
class UserSuggestionState(
|
||||||
val account: Account,
|
val account: Account,
|
||||||
@@ -105,9 +66,6 @@ class UserSuggestionState(
|
|||||||
val currentWord = MutableStateFlow("")
|
val currentWord = MutableStateFlow("")
|
||||||
val searchDataSourceState = SearchQueryState(MutableStateFlow(""), account)
|
val searchDataSourceState = SearchQueryState(MutableStateFlow(""), account)
|
||||||
|
|
||||||
/** Tracks Namecoin resolution status for the UI. */
|
|
||||||
val namecoinState = MutableStateFlow<NamecoinResolutionState>(NamecoinResolutionState.Idle)
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
val searchTerm =
|
val searchTerm =
|
||||||
currentWord
|
currentWord
|
||||||
@@ -124,38 +82,30 @@ class UserSuggestionState(
|
|||||||
.map(::userSearchTermOrNull)
|
.map(::userSearchTermOrNull)
|
||||||
.map { prefix ->
|
.map { prefix ->
|
||||||
if (prefix != null) {
|
if (prefix != null) {
|
||||||
val nip05 = toNip05IdOrNull(prefix)
|
// NIP-05 resolution: user@domain or bare .bit domain
|
||||||
|
val nip05 =
|
||||||
|
if (prefix.contains('@')) {
|
||||||
|
Nip05Id.parse(prefix)
|
||||||
|
} else if (prefix.endsWith(".bit", ignoreCase = true)) {
|
||||||
|
Nip05Id("_", prefix.lowercase())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
if (nip05 != null) {
|
if (nip05 != null) {
|
||||||
val isNamecoin = NamecoinNameResolver.isNamecoinIdentifier(nip05.toValue())
|
|
||||||
if (isNamecoin) namecoinState.emit(NamecoinResolutionState.Resolving)
|
|
||||||
|
|
||||||
val user =
|
|
||||||
runCatching {
|
runCatching {
|
||||||
nip05Client.get(nip05)?.let { info ->
|
nip05Client.get(nip05)?.let { info ->
|
||||||
val u = account.cache.checkGetOrCreateUser(info.pubkey)
|
val user = account.cache.checkGetOrCreateUser(info.pubkey)
|
||||||
if (u != null) {
|
if (user != null) {
|
||||||
info.relays.forEach {
|
info.relays.forEach {
|
||||||
it.normalizeRelayUrlOrNull()?.let { relay ->
|
it.normalizeRelayUrlOrNull()?.let { relay ->
|
||||||
account.cache.relayHints.addKey(u.pubkey(), relay)
|
account.cache.relayHints.addKey(user.pubkey(), relay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
if (isNamecoin) {
|
|
||||||
if (user != null) {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Resolved(user, prefix))
|
|
||||||
} else {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Error("Could not resolve $prefix via Namecoin"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
}
|
|
||||||
user
|
user
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
} else if (prefix.startsWithAny(userUriPrefixes)) {
|
} else if (prefix.startsWithAny(userUriPrefixes)) {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
runCatching {
|
runCatching {
|
||||||
Nip19Parser.uriToRoute(prefix)?.entity?.let { parsed ->
|
Nip19Parser.uriToRoute(prefix)?.entity?.let { parsed ->
|
||||||
when (parsed) {
|
when (parsed) {
|
||||||
@@ -182,14 +132,11 @@ class UserSuggestionState(
|
|||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
} else if (prefix.length == 64 && Hex.isHex64(prefix)) {
|
} else if (prefix.length == 64 && Hex.isHex64(prefix)) {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
account.cache.getOrCreateUser(prefix)
|
account.cache.getOrCreateUser(prefix)
|
||||||
} else {
|
} else {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|||||||
-119
@@ -20,11 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -42,7 +37,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PersonAdd
|
import androidx.compose.material.icons.filled.PersonAdd
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -73,7 +67,6 @@ import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDa
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.NamecoinResolutionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -107,10 +100,6 @@ class ImportFollowListSelectUserViewModel(
|
|||||||
userSuggestions.results
|
userSuggestions.results
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val namecoinState =
|
|
||||||
userSuggestions.namecoinState
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, NamecoinResolutionState.Idle)
|
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
val account: Account,
|
val account: Account,
|
||||||
val nip05Client: INip05Client,
|
val nip05Client: INip05Client,
|
||||||
@@ -177,10 +166,6 @@ private fun InputSelectUserBody(
|
|||||||
supportingText = { Text(stringRes(R.string.supports_npub_nip_05_hex_and_namecoin_bit_d_id)) },
|
supportingText = { Text(stringRes(R.string.supports_npub_nip_05_hex_and_namecoin_bit_d_id)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
|
|
||||||
NamecoinStatusBanner(viewModel)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
CustomShowUserSuggestionList(
|
CustomShowUserSuggestionList(
|
||||||
@@ -200,88 +185,6 @@ private fun InputSelectUserBody(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NamecoinStatusBanner(viewModel: ImportFollowListSelectUserViewModel) {
|
|
||||||
val ncState by viewModel.namecoinState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = ncState !is NamecoinResolutionState.Idle,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
when (val state = ncState) {
|
|
||||||
is NamecoinResolutionState.Resolving -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
"Resolving via Namecoin blockchain\u2026",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Resolved -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"\u26D3\uFE0F",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
"Resolved via Namecoin: ${state.namecoinName}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Error -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"\u26A0\uFE0F",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
state.message,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Idle -> {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ImportHeader() {
|
private fun ImportHeader() {
|
||||||
Column {
|
Column {
|
||||||
@@ -341,7 +244,6 @@ fun CustomWatchResponses(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val suggestions by viewModel.results.collectAsStateWithLifecycle()
|
val suggestions by viewModel.results.collectAsStateWithLifecycle()
|
||||||
val ncState by viewModel.namecoinState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
if (suggestions.isNotEmpty()) {
|
if (suggestions.isNotEmpty()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -350,28 +252,7 @@ fun CustomWatchResponses(
|
|||||||
state = viewModel.listState,
|
state = viewModel.listState,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||||
val isNamecoinResult =
|
|
||||||
ncState is NamecoinResolutionState.Resolved &&
|
|
||||||
(ncState as NamecoinResolutionState.Resolved).user == item
|
|
||||||
|
|
||||||
if (isNamecoinResult) {
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"\u26D3\uFE0F Namecoin result",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
UserLine(item, accountViewModel) { onSelect(item) }
|
UserLine(item, accountViewModel) { onSelect(item) }
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UserLine(item, accountViewModel) { onSelect(item) }
|
|
||||||
}
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = DividerThickness,
|
thickness = DividerThickness,
|
||||||
)
|
)
|
||||||
|
|||||||
+20
-42
@@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.NamecoinResolutionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.userUriPrefixes
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.userUriPrefixes
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
@@ -44,7 +43,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
|
||||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||||
@@ -59,6 +57,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
@@ -68,26 +67,6 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
/** Returns a [Nip05Id] for identifiers that should go through NIP-05 / Namecoin resolution. */
|
|
||||||
private fun toNip05IdOrNull(term: String): Nip05Id? =
|
|
||||||
when {
|
|
||||||
term.contains('@') -> {
|
|
||||||
Nip05Id.parse(term)
|
|
||||||
}
|
|
||||||
|
|
||||||
NamecoinNameResolver.isNamecoinIdentifier(term) -> {
|
|
||||||
if (term.endsWith(".bit", ignoreCase = true)) {
|
|
||||||
Nip05Id("_", term.lowercase())
|
|
||||||
} else {
|
|
||||||
Nip05Id("_", term.lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
class SearchBarViewModel(
|
class SearchBarViewModel(
|
||||||
@@ -101,9 +80,6 @@ class SearchBarViewModel(
|
|||||||
val invalidations = MutableStateFlow(0)
|
val invalidations = MutableStateFlow(0)
|
||||||
val searchValueFlow = MutableStateFlow("")
|
val searchValueFlow = MutableStateFlow("")
|
||||||
|
|
||||||
/** Tracks Namecoin resolution status for the UI. */
|
|
||||||
val namecoinState = MutableStateFlow<NamecoinResolutionState>(NamecoinResolutionState.Idle)
|
|
||||||
|
|
||||||
val searchTerm =
|
val searchTerm =
|
||||||
searchValueFlow
|
searchValueFlow
|
||||||
.debounce(300)
|
.debounce(300)
|
||||||
@@ -115,16 +91,27 @@ class SearchBarViewModel(
|
|||||||
|
|
||||||
val listState: LazyListState = LazyListState(0, 0)
|
val listState: LazyListState = LazyListState(0, 0)
|
||||||
|
|
||||||
|
/** The user resolved via Namecoin (.bit), if any, for the current search. */
|
||||||
|
private val _namecoinResolvedUser = MutableStateFlow<User?>(null)
|
||||||
|
val namecoinResolvedUser: StateFlow<User?> = _namecoinResolvedUser
|
||||||
|
|
||||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
val directNip05Resolver: Flow<User?> =
|
val directNip05Resolver: Flow<User?> =
|
||||||
searchTerm
|
searchTerm
|
||||||
.debounce(400)
|
.debounce(400)
|
||||||
.mapLatest { term ->
|
.mapLatest { term ->
|
||||||
val nip05 = toNip05IdOrNull(term)
|
// NIP-05 resolution: user@domain or bare .bit domain
|
||||||
|
val nip05 =
|
||||||
|
if (term.contains('@')) {
|
||||||
|
Nip05Id.parse(term)
|
||||||
|
} else if (term.endsWith(".bit", ignoreCase = true)) {
|
||||||
|
// Bare .bit domain → synthesize _@domain.bit
|
||||||
|
Nip05Id("_", term.lowercase())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val isNamecoin = nip05 != null && nip05.domain.endsWith(".bit", ignoreCase = true)
|
||||||
if (nip05 != null) {
|
if (nip05 != null) {
|
||||||
val isNamecoin = NamecoinNameResolver.isNamecoinIdentifier(nip05.toValue())
|
|
||||||
if (isNamecoin) namecoinState.emit(NamecoinResolutionState.Resolving)
|
|
||||||
|
|
||||||
val user =
|
val user =
|
||||||
runCatching {
|
runCatching {
|
||||||
nip05Client.get(nip05)?.let { info ->
|
nip05Client.get(nip05)?.let { info ->
|
||||||
@@ -139,19 +126,10 @@ class SearchBarViewModel(
|
|||||||
u
|
u
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
_namecoinResolvedUser.value = if (isNamecoin) user else null
|
||||||
if (isNamecoin) {
|
|
||||||
if (user != null) {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Resolved(user, term))
|
|
||||||
} else {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Error("Could not resolve $term via Namecoin"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
|
||||||
}
|
|
||||||
user
|
user
|
||||||
} else if (term.startsWithAny(userUriPrefixes)) {
|
} else if (term.startsWithAny(userUriPrefixes)) {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
_namecoinResolvedUser.value = null
|
||||||
runCatching {
|
runCatching {
|
||||||
Nip19Parser.uriToRoute(term)?.entity?.let { parsed ->
|
Nip19Parser.uriToRoute(term)?.entity?.let { parsed ->
|
||||||
when (parsed) {
|
when (parsed) {
|
||||||
@@ -178,10 +156,10 @@ class SearchBarViewModel(
|
|||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
} else if (term.length == 64 && Hex.isHex64(term)) {
|
} else if (term.length == 64 && Hex.isHex64(term)) {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
_namecoinResolvedUser.value = null
|
||||||
account.cache.getOrCreateUser(term)
|
account.cache.getOrCreateUser(term)
|
||||||
} else {
|
} else {
|
||||||
namecoinState.emit(NamecoinResolutionState.Idle)
|
_namecoinResolvedUser.value = null
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|||||||
+4
-107
@@ -20,29 +20,20 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.search
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.search
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -76,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
|||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.NamecoinResolutionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow
|
||||||
@@ -170,10 +160,7 @@ private fun SearchBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
|
||||||
SearchTextField(searchBarViewModel, Modifier.statusBarsPadding())
|
SearchTextField(searchBarViewModel, Modifier.statusBarsPadding())
|
||||||
SearchNamecoinStatusBanner(searchBarViewModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -229,88 +216,6 @@ private fun SearchTextField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SearchNamecoinStatusBanner(searchBarViewModel: SearchBarViewModel) {
|
|
||||||
val ncState by searchBarViewModel.namecoinState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = ncState !is NamecoinResolutionState.Idle,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
when (val state = ncState) {
|
|
||||||
is NamecoinResolutionState.Resolving -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 14.dp, vertical = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(14.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
"Resolving via Namecoin blockchain\u2026",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Resolved -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 14.dp, vertical = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"\u26D3\uFE0F",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
"Resolved via Namecoin: ${state.namecoinName}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Error -> {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 14.dp, vertical = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"\u26A0\uFE0F",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
state.message,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is NamecoinResolutionState.Idle -> {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisplaySearchResults(
|
private fun DisplaySearchResults(
|
||||||
searchBarViewModel: SearchBarViewModel,
|
searchBarViewModel: SearchBarViewModel,
|
||||||
@@ -324,11 +229,11 @@ private fun DisplaySearchResults(
|
|||||||
val hashTags by searchBarViewModel.hashtagResults.collectAsStateWithLifecycle()
|
val hashTags by searchBarViewModel.hashtagResults.collectAsStateWithLifecycle()
|
||||||
val relays by searchBarViewModel.relayResults.collectAsStateWithLifecycle()
|
val relays by searchBarViewModel.relayResults.collectAsStateWithLifecycle()
|
||||||
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
|
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
|
||||||
|
val namecoinUser by searchBarViewModel.namecoinResolvedUser.collectAsStateWithLifecycle()
|
||||||
val publicChatChannels by searchBarViewModel.searchResultsPublicChatChannels.collectAsStateWithLifecycle()
|
val publicChatChannels by searchBarViewModel.searchResultsPublicChatChannels.collectAsStateWithLifecycle()
|
||||||
val ephemeralChannels by searchBarViewModel.searchResultsEphemeralChannels.collectAsStateWithLifecycle()
|
val ephemeralChannels by searchBarViewModel.searchResultsEphemeralChannels.collectAsStateWithLifecycle()
|
||||||
val liveActivityChannels by searchBarViewModel.searchResultsLiveActivityChannels.collectAsStateWithLifecycle()
|
val liveActivityChannels by searchBarViewModel.searchResultsLiveActivityChannels.collectAsStateWithLifecycle()
|
||||||
val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle()
|
val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle()
|
||||||
val ncState by searchBarViewModel.namecoinState.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
@@ -351,23 +256,15 @@ private fun DisplaySearchResults(
|
|||||||
users,
|
users,
|
||||||
key = { _, item -> "u" + item.pubkeyHex },
|
key = { _, item -> "u" + item.pubkeyHex },
|
||||||
) { _, item ->
|
) { _, item ->
|
||||||
val isNamecoinResult =
|
if (namecoinUser == item) {
|
||||||
ncState is NamecoinResolutionState.Resolved &&
|
|
||||||
(ncState as NamecoinResolutionState.Resolved).user == item
|
|
||||||
|
|
||||||
if (isNamecoinResult) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
"\u26D3\uFE0F Namecoin result",
|
"\u26D3\uFE0F Namecoin",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
UserCompose(item, accountViewModel = accountViewModel, nav = nav)
|
UserCompose(item, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
|
||||||
|
|||||||
@@ -1820,7 +1820,7 @@
|
|||||||
<string name="select_users_to_follow">Select Users to Follow</string>
|
<string name="select_users_to_follow">Select Users to Follow</string>
|
||||||
<string name="profile_to_import_from">Profile to import from</string>
|
<string name="profile_to_import_from">Profile to import from</string>
|
||||||
<string name="name_search_npub1_alice_example_com">search, npub1…, alice@example.com</string>
|
<string name="name_search_npub1_alice_example_com">search, npub1…, alice@example.com</string>
|
||||||
<string name="supports_npub_nip_05_hex_and_namecoin_bit_d_id">Supports npub, nprofile, NIP-05, hex, and namecoin (.bit, d/, id/)</string>
|
<string name="supports_npub_nip_05_hex_and_namecoin_bit_d_id">Supports npub, nprofile, NIP-05, hex, and Namecoin (.bit)</string>
|
||||||
<string name="look_up_follow_list">Look Up Follow List</string>
|
<string name="look_up_follow_list">Look Up Follow List</string>
|
||||||
<string name="tip">Tip</string>
|
<string name="tip">Tip</string>
|
||||||
<string name="accounts_found">%1$d accounts found</string>
|
<string name="accounts_found">%1$d accounts found</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user