fix(desktop): use SearchBarState + rememberSubscription for author search

Replace manual relay subscribe + Channel approach with the proven
SearchBarState + rememberSubscription pattern (same as NewDmDialog).
This properly handles relay connection lifecycle and NIP-50 search,
returning results from all connected relays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-05-08 07:32:20 +03:00
parent 41920a363a
commit de879b9472
2 changed files with 448 additions and 288 deletions
@@ -20,46 +20,52 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material3.AlertDialog
import androidx.compose.material.icons.outlined.AutoAwesome import androidx.compose.material3.Button
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material3.Card
import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material3.CardDefaults
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.GroupAdd
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.MilitaryTech
import androidx.compose.material.icons.outlined.Phone
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.ThumbUp
import androidx.compose.material.icons.outlined.Translate
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
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
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
@@ -69,6 +75,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Preview @Preview
@Composable @Composable
@@ -86,223 +94,418 @@ fun AllSettingsScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val tint = MaterialTheme.colorScheme.onBackground val context = LocalContext.current
val scope = rememberCoroutineScope()
var showResetMarmotDialog by remember { mutableStateOf(false) }
var isResettingMarmot by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
val hasPrivateKey = accountViewModel.account.settings.keyPair.privKey != null
Scaffold( Scaffold(
topBar = { topBar = {
TopBarWithBackButton(stringRes(id = R.string.settings), nav::popBack) TopBarWithBackButton(stringRes(id = R.string.settings), nav)
},
bottomBar = {
AppBottomBar(Route.AllSettings, nav, accountViewModel) { route ->
if (route == Route.AllSettings) {
scope.launch { scrollState.animateScrollTo(0) }
} else {
nav.navBottomBar(route)
}
}
}, },
) { padding -> ) { padding ->
Column(Modifier.padding(padding).verticalScroll(rememberScrollState())) { Column(
SettingsSectionHeader(R.string.account_settings) modifier =
SettingsNavigationRow( Modifier
title = R.string.relay_setup, .padding(padding)
iconPainter = R.drawable.relays, .verticalScroll(scrollState)
iconPainterRef = 4, .padding(horizontal = 16.dp, vertical = 12.dp),
tint = tint, verticalArrangement = Arrangement.spacedBy(20.dp),
onClick = { nav.nav(Route.EditRelays) }, ) {
) SettingsSection(R.string.account_settings) {
HorizontalDivider() SettingsItem(
SettingsNavigationRow( title = R.string.relay_setup,
title = R.string.event_sync_title, iconPainter = R.drawable.relays,
icon = Icons.Outlined.Sync, iconPainterRef = 4,
tint = tint, onClick = { nav.nav(Route.EditRelays) },
onClick = { nav.nav(Route.EventSync) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.route_import_follows,
icon = Icons.Outlined.GroupAdd,
tint = tint,
onClick = { nav.nav(Route.ImportFollowsSelectUser) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.media_servers,
icon = Icons.Outlined.CloudUpload,
tint = tint,
onClick = { nav.nav(Route.EditMediaServers) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.profile_badges_title,
icon = Icons.Outlined.MilitaryTech,
tint = tint,
onClick = { nav.nav(Route.ProfileBadges) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.favorite_dvms_title,
icon = Icons.Outlined.AutoAwesome,
tint = tint,
onClick = { nav.nav(Route.EditFavoriteAlgoFeeds) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions,
icon = Icons.Outlined.FavoriteBorder,
tint = tint,
onClick = { nav.nav(Route.UpdateReactionType) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.zaps,
icon = Icons.Outlined.Bolt,
tint = tint,
onClick = { nav.nav(Route.UpdateZapAmount()) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.security_filters,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { nav.nav(Route.SecurityFilters) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.call_settings,
icon = Icons.Outlined.Phone,
tint = tint,
onClick = { nav.nav(Route.CallSettings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.translations,
icon = Icons.Outlined.Translate,
tint = tint,
onClick = { nav.nav(Route.UserSettings) },
)
HorizontalDivider(thickness = 4.dp)
SettingsSectionHeader(R.string.app_settings)
SettingsNavigationRow(
title = R.string.privacy_options,
iconPainter = R.drawable.ic_tor,
iconPainterRef = 1,
tint = tint,
onClick = { nav.nav(Route.PrivacyOptions) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.ots_explorer_settings,
icon = Icons.Outlined.Search,
tint = tint,
onClick = { nav.nav(Route.OtsSettings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.namecoin_settings,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { nav.nav(Route.NamecoinSettings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.ui_preferences,
icon = Icons.Outlined.Settings,
tint = tint,
onClick = { nav.nav(Route.Settings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions_settings,
icon = Icons.Outlined.ThumbUp,
tint = tint,
onClick = { nav.nav(Route.ReactionsSettings) },
)
HorizontalDivider(thickness = 4.dp)
SettingsSectionHeader(R.string.danger_zone)
accountViewModel.account.settings.keyPair.privKey?.let {
SettingsNavigationRow(
title = R.string.backup_keys,
icon = Icons.Outlined.Key,
tint = tint,
onClick = { nav.nav(Route.AccountBackup) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.request_to_vanish, title = R.string.event_sync_title,
icon = Icons.Outlined.DeleteForever, icon = MaterialSymbols.Sync,
tint = tint, onClick = { nav.nav(Route.EventSync) },
onClick = { nav.nav(Route.RequestToVanish) }, )
SettingsDivider()
SettingsItem(
title = R.string.route_import_follows,
icon = MaterialSymbols.GroupAdd,
onClick = { nav.nav(Route.ImportFollowsSelectUser) },
)
SettingsDivider()
SettingsItem(
title = R.string.media_servers,
icon = MaterialSymbols.CloudUpload,
onClick = { nav.nav(Route.EditMediaServers) },
)
SettingsDivider()
SettingsItem(
title = R.string.nests_servers_title,
icon = MaterialSymbols.CloudUpload,
onClick = { nav.nav(Route.EditNestsServers) },
)
SettingsDivider()
SettingsItem(
title = R.string.profile_badges_title,
icon = MaterialSymbols.MilitaryTech,
onClick = { nav.nav(Route.ProfileBadges) },
)
SettingsDivider()
SettingsItem(
title = R.string.favorite_dvms_title,
icon = MaterialSymbols.AutoAwesome,
onClick = { nav.nav(Route.EditFavoriteAlgoFeeds) },
)
SettingsDivider()
SettingsItem(
title = R.string.reactions,
icon = MaterialSymbols.FavoriteBorder,
onClick = { nav.nav(Route.UpdateReactionType) },
)
SettingsDivider()
SettingsItem(
title = R.string.video_player_settings,
icon = MaterialSymbols.VideoSettings,
onClick = { nav.nav(Route.VideoPlayerSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.zaps,
icon = MaterialSymbols.Bolt,
onClick = { nav.nav(Route.UpdateZapAmount()) },
)
SettingsDivider()
SettingsItem(
title = R.string.payment_targets,
icon = MaterialSymbols.Payment,
onClick = { nav.nav(Route.EditPaymentTargets) },
)
SettingsDivider()
SettingsItem(
title = R.string.security_filters,
icon = MaterialSymbols.Security,
onClick = { nav.nav(Route.SecurityFilters) },
)
SettingsDivider()
SettingsItem(
title = R.string.call_settings,
icon = MaterialSymbols.Phone,
onClick = { nav.nav(Route.CallSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.translations,
icon = MaterialSymbols.Translate,
onClick = { nav.nav(Route.UserSettings) },
) )
HorizontalDivider()
} }
SettingsNavigationRow(
title = R.string.vanish_history, SettingsSection(R.string.app_settings) {
icon = Icons.Outlined.History, SettingsItem(
tint = tint, title = R.string.privacy_options,
onClick = { nav.nav(Route.VanishEvents) }, iconPainter = R.drawable.ic_tor,
iconPainterRef = 1,
onClick = { nav.nav(Route.PrivacyOptions) },
)
SettingsDivider()
SettingsItem(
title = R.string.ots_explorer_settings,
icon = MaterialSymbols.Search,
onClick = { nav.nav(Route.OtsSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.namecoin_settings,
icon = MaterialSymbols.Security,
onClick = { nav.nav(Route.NamecoinSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.ui_preferences,
icon = MaterialSymbols.Settings,
onClick = { nav.nav(Route.Settings) },
)
SettingsDivider()
SettingsItem(
title = R.string.reactions_settings,
icon = MaterialSymbols.ThumbUp,
onClick = { nav.nav(Route.ReactionsSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.bottom_bar_settings,
icon = MaterialSymbols.Dashboard,
onClick = { nav.nav(Route.BottomBarSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.home_tabs_settings,
icon = MaterialSymbols.Home,
onClick = { nav.nav(Route.HomeTabsSettings) },
)
}
SettingsSection(R.string.danger_zone, isDanger = true) {
if (hasPrivateKey) {
SettingsItem(
title = R.string.backup_keys,
icon = MaterialSymbols.Key,
isDanger = true,
onClick = { nav.nav(Route.AccountBackup) },
)
SettingsDivider()
SettingsItem(
title = R.string.request_to_vanish,
icon = MaterialSymbols.DeleteForever,
isDanger = true,
onClick = { nav.nav(Route.RequestToVanish) },
)
SettingsDivider()
}
SettingsItem(
title = R.string.vanish_history,
icon = MaterialSymbols.History,
isDanger = true,
onClick = { nav.nav(Route.VanishEvents) },
)
SettingsDivider()
SettingsItem(
title = R.string.reset_marmot_state,
icon = MaterialSymbols.DeleteSweep,
isDanger = true,
onClick = { if (!isResettingMarmot) showResetMarmotDialog = true },
)
}
}
}
if (showResetMarmotDialog) {
ResetMarmotStateDialog(
onConfirm = {
showResetMarmotDialog = false
isResettingMarmot = true
scope.launch(Dispatchers.IO) {
val successMessage = stringRes(context, R.string.reset_marmot_success)
try {
accountViewModel.resetMarmotState()
launch(Dispatchers.Main) {
Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
val failureMessage =
stringRes(context, R.string.reset_marmot_failure, e.message ?: "")
launch(Dispatchers.Main) {
Toast.makeText(context, failureMessage, Toast.LENGTH_LONG).show()
}
} finally {
isResettingMarmot = false
}
}
},
onDismiss = { showResetMarmotDialog = false },
)
}
}
@Composable
private fun ResetMarmotStateDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
symbol = MaterialSymbols.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(32.dp),
) )
},
title = {
Text(
text = stringRes(R.string.reset_marmot_confirm_title),
textAlign = TextAlign.Center,
)
},
text = {
Text(text = stringRes(R.string.reset_marmot_confirm_body))
},
confirmButton = {
Button(
onClick = onConfirm,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
),
) {
Text(stringRes(R.string.reset_marmot_confirm_action))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringRes(R.string.cancel))
}
},
)
}
@Composable
private fun SettingsSection(
title: Int,
isDanger: Boolean = false,
content: @Composable ColumnScope.() -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringRes(title),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color =
if (isDanger) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
},
modifier = Modifier.padding(horizontal = 4.dp),
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
) {
Column(content = content)
} }
} }
} }
@Composable @Composable
private fun SettingsSectionHeader(title: Int) { private fun SettingsDivider() {
Text( HorizontalDivider(
text = stringRes(title), modifier = Modifier.padding(start = 68.dp),
fontSize = 12.sp, thickness = 0.5.dp,
fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.outlineVariant,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp),
) )
} }
@Composable @Composable
private fun SettingsNavigationRow( private fun SettingsItem(
title: Int, title: Int,
icon: ImageVector, icon: MaterialSymbol,
tint: Color, isDanger: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Row( SettingsItemRow(
modifier = title = title,
Modifier isDanger = isDanger,
.fillMaxWidth() onClick = onClick,
.clickable(onClick = onClick) leadingIcon = { tint ->
.padding(vertical = 16.dp, horizontal = 24.dp), Icon(
verticalAlignment = Alignment.CenterVertically, symbol = icon,
) { contentDescription = stringRes(title),
Icon( modifier = Modifier.size(20.dp),
imageVector = icon, tint = tint,
contentDescription = stringRes(title), )
modifier = Modifier.size(24.dp), },
tint = tint, )
)
Text(
text = stringRes(title),
fontSize = 18.sp,
modifier = Modifier.padding(start = 16.dp),
)
}
} }
@Composable @Composable
private fun SettingsNavigationRow( private fun SettingsItem(
title: Int, title: Int,
iconPainter: Int, iconPainter: Int,
iconPainterRef: Int, iconPainterRef: Int,
tint: Color, isDanger: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val painter: Painter = painterRes(iconPainter, iconPainterRef)
SettingsItemRow(
title = title,
isDanger = isDanger,
onClick = onClick,
leadingIcon = { tint ->
Icon(
painter = painter,
contentDescription = stringRes(title),
modifier = Modifier.size(20.dp),
tint = tint,
)
},
)
}
@Composable
private fun SettingsItemRow(
title: Int,
isDanger: Boolean,
onClick: () -> Unit,
leadingIcon: @Composable (tint: Color) -> Unit,
) {
val containerColor =
if (isDanger) {
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.primaryContainer
}
val iconTint =
if (isDanger) {
MaterialTheme.colorScheme.onErrorContainer
} else {
MaterialTheme.colorScheme.onPrimaryContainer
}
val textColor =
if (isDanger) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurface
}
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(vertical = 16.dp, horizontal = 24.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Box(
painter = painterRes(iconPainter, iconPainterRef), modifier =
contentDescription = stringRes(title), Modifier
modifier = Modifier.size(24.dp), .size(36.dp)
tint = tint, .clip(RoundedCornerShape(10.dp))
) .background(containerColor),
contentAlignment = Alignment.Center,
) {
leadingIcon(iconTint)
}
Text( Text(
text = stringRes(title), text = stringRes(title),
fontSize = 18.sp, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp), color = textColor,
modifier =
Modifier
.weight(1f)
.padding(start = 16.dp),
)
Icon(
symbol = MaterialSymbols.ChevronRight,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }
@@ -77,100 +77,57 @@ fun FeedsDrawerTab(
var editingFeed by remember { mutableStateOf<FeedDefinition?>(null) } var editingFeed by remember { mutableStateOf<FeedDefinition?>(null) }
var deletingFeed by remember { mutableStateOf<FeedDefinition?>(null) } var deletingFeed by remember { mutableStateOf<FeedDefinition?>(null) }
// Author search state — hoisted here because AlertDialog can't run LaunchedEffect // Author search — same pattern as NewDmDialog: SearchBarState + rememberSubscription
var authorQuery by remember { mutableStateOf("") } val searchState =
var authorLocal by remember { remember(localCache) {
mutableStateOf(emptyList<com.vitorpamplona.amethyst.commons.model.User>()) localCache?.let {
} com.vitorpamplona.amethyst.commons.viewmodels
var authorRelay by remember { .SearchBarState(it, scope)
mutableStateOf(emptyList<com.vitorpamplona.amethyst.commons.model.User>())
}
var authorSearching by remember { mutableStateOf(false) }
LaunchedEffect(authorQuery) {
if (authorQuery.length < 2 || localCache == null) {
authorLocal = emptyList()
authorRelay = emptyList()
authorSearching = false
return@LaunchedEffect
}
kotlinx.coroutines.delay(300)
val results = localCache.findUsersStartingWith(authorQuery, 10)
authorLocal = results
if (relayManager != null) {
// Use all connected relays — some support NIP-50 search
val relays = relayManager.connectedRelays.value
if (relays.isNotEmpty()) {
authorSearching = true
authorRelay = emptyList()
val ch = kotlinx.coroutines.channels.Channel<com.vitorpamplona.amethyst.commons.model.User>(64)
val subId =
com.vitorpamplona.amethyst.desktop.subscriptions
.generateSubId("author-search")
relayManager.subscribe(
subId = subId,
filters =
listOf(
com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders
.searchPeople(authorQuery, 30),
),
relays = relays,
listener =
object : com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener {
override fun onEvent(
event: com.vitorpamplona.quartz.nip01Core.core.Event,
isLive: Boolean,
relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl,
forFilters: List<com.vitorpamplona.quartz.nip01Core.relay.filters.Filter>?,
) {
if (event is com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent) {
localCache.consumeMetadata(event)
localCache.getUserIfExists(event.pubKey)?.let {
ch.trySend(it)
}
}
}
override fun onEose(
relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl,
forFilters: List<com.vitorpamplona.quartz.nip01Core.relay.filters.Filter>?,
) {
ch.close()
}
override fun onClosed(
message: String,
relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl,
forFilters: List<com.vitorpamplona.quartz.nip01Core.relay.filters.Filter>?,
) {
ch.close()
}
},
)
try {
kotlinx.coroutines.withTimeoutOrNull(8000) {
for (user in ch) {
if (authorRelay.none { it.pubkeyHex == user.pubkeyHex }) {
authorRelay = authorRelay + user
}
}
}
} finally {
authorSearching = false
relayManager.unsubscribe(subId)
}
} }
} }
val authorQuery = searchState?.searchText?.collectAsState()?.value ?: ""
val authorLocal = searchState?.cachedUserResults?.collectAsState()?.value ?: emptyList()
val authorRelay = searchState?.relaySearchResults?.collectAsState()?.value ?: emptyList()
val authorSearching = searchState?.isSearchingRelays?.collectAsState()?.value ?: false
// NIP-50 relay search — fires when local cache has few results (same as NewDmDialog)
if (relayManager != null && searchState != null) {
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = relayStatuses.keys
com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription(
connectedRelays,
authorQuery,
authorLocal.size,
relayManager = relayManager,
) {
if (connectedRelays.isEmpty()) return@rememberSubscription null
if (!searchState.shouldSearchRelays) return@rememberSubscription null
searchState.startRelaySearch()
com.vitorpamplona.amethyst.desktop.subscriptions.createSearchPeopleSubscription(
relays = connectedRelays,
searchQuery = authorQuery,
limit = 30,
onEvent = { event, _, _, _ ->
if (event is com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent &&
localCache != null
) {
localCache.consumeMetadata(event)
localCache.getUserIfExists(event.pubKey)?.let {
searchState.addRelaySearchResult(it)
}
}
},
onEose = { _, _ -> searchState.endRelaySearch() },
)
}
} }
// Reset search state when dialogs close // Reset search when dialogs close
LaunchedEffect(showBuilder, editingFeed) { LaunchedEffect(showBuilder, editingFeed) {
if (!showBuilder && editingFeed == null) { if (!showBuilder && editingFeed == null) {
authorQuery = "" searchState?.clearSearch()
authorLocal = emptyList()
authorRelay = emptyList()
authorSearching = false
} }
} }
@@ -179,7 +136,7 @@ fun FeedsDrawerTab(
FeedBuilderDialog( FeedBuilderDialog(
localCache = localCache, localCache = localCache,
authorQuery = authorQuery, authorQuery = authorQuery,
onAuthorQueryChange = { authorQuery = it }, onAuthorQueryChange = { searchState?.updateSearchText(it) },
authorSuggestions = authorLocal, authorSuggestions = authorLocal,
authorRelayResults = authorRelay, authorRelayResults = authorRelay,
authorSearching = authorSearching, authorSearching = authorSearching,
@@ -197,7 +154,7 @@ fun FeedsDrawerTab(
initial = feed, initial = feed,
localCache = localCache, localCache = localCache,
authorQuery = authorQuery, authorQuery = authorQuery,
onAuthorQueryChange = { authorQuery = it }, onAuthorQueryChange = { searchState?.updateSearchText(it) },
authorSuggestions = authorLocal, authorSuggestions = authorLocal,
authorRelayResults = authorRelay, authorRelayResults = authorRelay,
authorSearching = authorSearching, authorSearching = authorSearching,