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
.padding(padding)
.verticalScroll(scrollState)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
SettingsSection(R.string.account_settings) {
SettingsItem(
title = R.string.relay_setup, title = R.string.relay_setup,
iconPainter = R.drawable.relays, iconPainter = R.drawable.relays,
iconPainterRef = 4, iconPainterRef = 4,
tint = tint,
onClick = { nav.nav(Route.EditRelays) }, onClick = { nav.nav(Route.EditRelays) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.event_sync_title, title = R.string.event_sync_title,
icon = Icons.Outlined.Sync, icon = MaterialSymbols.Sync,
tint = tint,
onClick = { nav.nav(Route.EventSync) }, onClick = { nav.nav(Route.EventSync) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.route_import_follows, title = R.string.route_import_follows,
icon = Icons.Outlined.GroupAdd, icon = MaterialSymbols.GroupAdd,
tint = tint,
onClick = { nav.nav(Route.ImportFollowsSelectUser) }, onClick = { nav.nav(Route.ImportFollowsSelectUser) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.media_servers, title = R.string.media_servers,
icon = Icons.Outlined.CloudUpload, icon = MaterialSymbols.CloudUpload,
tint = tint,
onClick = { nav.nav(Route.EditMediaServers) }, onClick = { nav.nav(Route.EditMediaServers) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.nests_servers_title,
icon = MaterialSymbols.CloudUpload,
onClick = { nav.nav(Route.EditNestsServers) },
)
SettingsDivider()
SettingsItem(
title = R.string.profile_badges_title, title = R.string.profile_badges_title,
icon = Icons.Outlined.MilitaryTech, icon = MaterialSymbols.MilitaryTech,
tint = tint,
onClick = { nav.nav(Route.ProfileBadges) }, onClick = { nav.nav(Route.ProfileBadges) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.favorite_dvms_title, title = R.string.favorite_dvms_title,
icon = Icons.Outlined.AutoAwesome, icon = MaterialSymbols.AutoAwesome,
tint = tint,
onClick = { nav.nav(Route.EditFavoriteAlgoFeeds) }, onClick = { nav.nav(Route.EditFavoriteAlgoFeeds) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.reactions, title = R.string.reactions,
icon = Icons.Outlined.FavoriteBorder, icon = MaterialSymbols.FavoriteBorder,
tint = tint,
onClick = { nav.nav(Route.UpdateReactionType) }, onClick = { nav.nav(Route.UpdateReactionType) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.video_player_settings,
icon = MaterialSymbols.VideoSettings,
onClick = { nav.nav(Route.VideoPlayerSettings) },
)
SettingsDivider()
SettingsItem(
title = R.string.zaps, title = R.string.zaps,
icon = Icons.Outlined.Bolt, icon = MaterialSymbols.Bolt,
tint = tint,
onClick = { nav.nav(Route.UpdateZapAmount()) }, onClick = { nav.nav(Route.UpdateZapAmount()) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.payment_targets,
icon = MaterialSymbols.Payment,
onClick = { nav.nav(Route.EditPaymentTargets) },
)
SettingsDivider()
SettingsItem(
title = R.string.security_filters, title = R.string.security_filters,
icon = Icons.Outlined.Security, icon = MaterialSymbols.Security,
tint = tint,
onClick = { nav.nav(Route.SecurityFilters) }, onClick = { nav.nav(Route.SecurityFilters) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.call_settings, title = R.string.call_settings,
icon = Icons.Outlined.Phone, icon = MaterialSymbols.Phone,
tint = tint,
onClick = { nav.nav(Route.CallSettings) }, onClick = { nav.nav(Route.CallSettings) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.translations, title = R.string.translations,
icon = Icons.Outlined.Translate, icon = MaterialSymbols.Translate,
tint = tint,
onClick = { nav.nav(Route.UserSettings) }, onClick = { nav.nav(Route.UserSettings) },
) )
HorizontalDivider(thickness = 4.dp) }
SettingsSectionHeader(R.string.app_settings)
SettingsNavigationRow( SettingsSection(R.string.app_settings) {
SettingsItem(
title = R.string.privacy_options, title = R.string.privacy_options,
iconPainter = R.drawable.ic_tor, iconPainter = R.drawable.ic_tor,
iconPainterRef = 1, iconPainterRef = 1,
tint = tint,
onClick = { nav.nav(Route.PrivacyOptions) }, onClick = { nav.nav(Route.PrivacyOptions) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.ots_explorer_settings, title = R.string.ots_explorer_settings,
icon = Icons.Outlined.Search, icon = MaterialSymbols.Search,
tint = tint,
onClick = { nav.nav(Route.OtsSettings) }, onClick = { nav.nav(Route.OtsSettings) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.namecoin_settings, title = R.string.namecoin_settings,
icon = Icons.Outlined.Security, icon = MaterialSymbols.Security,
tint = tint,
onClick = { nav.nav(Route.NamecoinSettings) }, onClick = { nav.nav(Route.NamecoinSettings) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.ui_preferences, title = R.string.ui_preferences,
icon = Icons.Outlined.Settings, icon = MaterialSymbols.Settings,
tint = tint,
onClick = { nav.nav(Route.Settings) }, onClick = { nav.nav(Route.Settings) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.reactions_settings, title = R.string.reactions_settings,
icon = Icons.Outlined.ThumbUp, icon = MaterialSymbols.ThumbUp,
tint = tint,
onClick = { nav.nav(Route.ReactionsSettings) }, onClick = { nav.nav(Route.ReactionsSettings) },
) )
HorizontalDivider(thickness = 4.dp) SettingsDivider()
SettingsSectionHeader(R.string.danger_zone) SettingsItem(
accountViewModel.account.settings.keyPair.privKey?.let { title = R.string.bottom_bar_settings,
SettingsNavigationRow( 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, title = R.string.backup_keys,
icon = Icons.Outlined.Key, icon = MaterialSymbols.Key,
tint = tint, isDanger = true,
onClick = { nav.nav(Route.AccountBackup) }, onClick = { nav.nav(Route.AccountBackup) },
) )
HorizontalDivider() SettingsDivider()
SettingsNavigationRow( SettingsItem(
title = R.string.request_to_vanish, title = R.string.request_to_vanish,
icon = Icons.Outlined.DeleteForever, icon = MaterialSymbols.DeleteForever,
tint = tint, isDanger = true,
onClick = { nav.nav(Route.RequestToVanish) }, onClick = { nav.nav(Route.RequestToVanish) },
) )
HorizontalDivider() SettingsDivider()
} }
SettingsNavigationRow( SettingsItem(
title = R.string.vanish_history, title = R.string.vanish_history,
icon = Icons.Outlined.History, icon = MaterialSymbols.History,
tint = tint, isDanger = true,
onClick = { nav.nav(Route.VanishEvents) }, 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 @Composable
private fun SettingsSectionHeader(title: Int) { 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(
text = stringRes(title), text = stringRes(R.string.reset_marmot_confirm_title),
fontSize = 12.sp, textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold, )
color = MaterialTheme.colorScheme.primary, },
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp), 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 @Composable
private fun SettingsNavigationRow( private fun SettingsSection(
title: Int, title: Int,
icon: ImageVector, isDanger: Boolean = false,
tint: Color, 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
private fun SettingsDivider() {
HorizontalDivider(
modifier = Modifier.padding(start = 68.dp),
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
}
@Composable
private fun SettingsItem(
title: Int,
icon: MaterialSymbol,
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),
verticalAlignment = Alignment.CenterVertically,
) {
Icon( Icon(
imageVector = icon, symbol = icon,
contentDescription = stringRes(title), contentDescription = stringRes(title),
modifier = Modifier.size(24.dp), modifier = Modifier.size(20.dp),
tint = tint, 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
.SearchBarState(it, scope)
} }
var authorRelay by remember {
mutableStateOf(emptyList<com.vitorpamplona.amethyst.commons.model.User>())
} }
var authorSearching by remember { mutableStateOf(false) } 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
LaunchedEffect(authorQuery) { // NIP-50 relay search — fires when local cache has few results (same as NewDmDialog)
if (authorQuery.length < 2 || localCache == null) { if (relayManager != null && searchState != null) {
authorLocal = emptyList() val relayStatuses by relayManager.relayStatuses.collectAsState()
authorRelay = emptyList() val connectedRelays = relayStatuses.keys
authorSearching = false
return@LaunchedEffect
}
kotlinx.coroutines.delay(300)
val results = localCache.findUsersStartingWith(authorQuery, 10)
authorLocal = results
if (relayManager != null) { com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription(
// Use all connected relays — some support NIP-50 search connectedRelays,
val relays = relayManager.connectedRelays.value authorQuery,
if (relays.isNotEmpty()) { authorLocal.size,
authorSearching = true relayManager = relayManager,
authorRelay = emptyList() ) {
val ch = kotlinx.coroutines.channels.Channel<com.vitorpamplona.amethyst.commons.model.User>(64) if (connectedRelays.isEmpty()) return@rememberSubscription null
val subId = if (!searchState.shouldSearchRelays) return@rememberSubscription null
com.vitorpamplona.amethyst.desktop.subscriptions
.generateSubId("author-search") searchState.startRelaySearch()
relayManager.subscribe( com.vitorpamplona.amethyst.desktop.subscriptions.createSearchPeopleSubscription(
subId = subId, relays = connectedRelays,
filters = searchQuery = authorQuery,
listOf( limit = 30,
com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders onEvent = { event, _, _, _ ->
.searchPeople(authorQuery, 30), if (event is com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent &&
), localCache != null
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.consumeMetadata(event)
localCache.getUserIfExists(event.pubKey)?.let { localCache.getUserIfExists(event.pubKey)?.let {
ch.trySend(it) searchState.addRelaySearchResult(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()
}
}, },
onEose = { _, _ -> searchState.endRelaySearch() },
) )
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)
}
}
} }
} }
// 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,