From eaa6c147e4a6af4db0ea910c5e244c475b5f8de1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 02:40:00 +0000 Subject: [PATCH] feat: add a search box to the App Recommendations screen The Recommended Apps screen listed every discovered app definition in a single sorted LazyColumn with no way to filter, so finding a specific app to recommend meant scrolling the whole list. Users couldn't find a search because there wasn't one. Add an always-visible outlined search field below the description that filters the list by app name (case-insensitive). Rows whose kind 31990 definition hasn't arrived yet have no name to match, so they only appear when the box is empty. A dedicated empty state shows when a query matches nothing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01EjuK6g3KNkyEEnti6JtDEi --- .../ProfileAppRecommendationsScreen.kt | 67 ++++++++++++++++++- amethyst/src/main/res/values/strings.xml | 2 + 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt index c7d403f383..aab7d0a200 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt @@ -32,7 +32,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -62,11 +64,15 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.note.ClearTextIcon +import com.vitorpamplona.amethyst.ui.note.SearchIcon import com.vitorpamplona.amethyst.ui.note.types.ByAuthorChip import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.apps.recommendations.datasource.ProfileAppRecommendationsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindDisplayName import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -156,6 +162,29 @@ fun ProfileAppRecommendationsScreen( .map { LocalCache.getOrCreateAddressableNote(it) } } + // Full candidate list in display order; the search box filters this view. + val allApps = + remember(missingRecommended, apps) { + missingRecommended + apps + } + + var searchQuery by remember { mutableStateOf("") } + + // Matches on the app's name; rows whose definition hasn't arrived yet have no + // name to match, so they only show when the search box is empty. + val visibleApps = + remember(allApps, searchQuery) { + val query = searchQuery.trim() + if (query.isEmpty()) { + allApps + } else { + allApps.filter { note -> + val event = note.event as? AppDefinitionEvent + event?.appMetaData()?.anyName()?.contains(query, ignoreCase = true) == true + } + } + } + Scaffold( topBar = { TopBarWithBackButton(stringRes(id = R.string.profile_app_recommendations_title), nav) @@ -168,19 +197,53 @@ fun ProfileAppRecommendationsScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), ) + + if (allApps.isNotEmpty()) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 4.dp), + placeholder = { + Text( + text = stringRes(R.string.profile_app_recommendations_search), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + leadingIcon = { SearchIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + ClearTextIcon() + } + } + }, + singleLine = true, + ) + } + HorizontalDivider() - if (apps.isEmpty() && missingRecommended.isEmpty()) { + if (allApps.isEmpty()) { Text( text = stringRes(R.string.profile_app_recommendations_empty), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(20.dp), ) + } else if (visibleApps.isEmpty()) { + Text( + text = stringRes(R.string.profile_app_recommendations_search_empty), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(20.dp), + ) } else { LazyColumn(modifier = Modifier.fillMaxSize()) { items( - items = missingRecommended + apps, + items = visibleApps, key = { it.idHex }, ) { appNote -> AppRow( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e5ce712d1c..551d7b9d84 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -604,6 +604,8 @@ Recommended apps Choose which Nostr apps you publicly recommend. Recommendations appear on your profile and help others discover apps for content this client can\'t open. No apps found yet. Apps will appear here as they are discovered on your relays. + Search apps by name + No apps match your search. Unnamed app Doesn\'t announce what content it handles Recommend