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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjuK6g3KNkyEEnti6JtDEi
This commit is contained in:
Claude
2026-06-21 02:40:00 +00:00
parent 6227cfd220
commit eaa6c147e4
2 changed files with 67 additions and 2 deletions
@@ -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(
+2
View File
@@ -604,6 +604,8 @@
<string name="profile_app_recommendations_title">Recommended apps</string>
<string name="profile_app_recommendations_description">Choose which Nostr apps you publicly recommend. Recommendations appear on your profile and help others discover apps for content this client can\'t open.</string>
<string name="profile_app_recommendations_empty">No apps found yet. Apps will appear here as they are discovered on your relays.</string>
<string name="profile_app_recommendations_search">Search apps by name</string>
<string name="profile_app_recommendations_search_empty">No apps match your search.</string>
<string name="app_definition_untitled">Unnamed app</string>
<string name="app_definition_no_supported_kinds">Doesn\'t announce what content it handles</string>
<string name="app_definition_recommend">Recommend</string>