feat(search): give the desktop spotlight its people picker

Typing `from:` or `to:` in the spotlight opened nothing — the field was
constructed without `people`, `displayName` or `onPeopleQuery`, so the
picker had no rows to offer and a key had to be pasted in as a raw npub.

It now builds the same UserSearchEngine the full search screen does, with
the same DesktopRelayUserSearchDelegate behind it: cache hits first, then
whatever the account's search relays answer with. The built-in `people`
list is used rather than the `peoplePicker` slot, because that path is the
keyboard-walkable one — arrows walk the rows and Enter takes the highlighted
one, which is what a spotlight is driven by. `displayName` also gives
finished key chips a name instead of a short npub.

The input row switches from centre to top alignment. The picker opens below
the field inside the same column, so centring would drag the leading search
icon halfway down it; with no picker up the two are the same height and the
resting layout is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yKjw2WqwZpSzsqcYZMnkV
This commit is contained in:
Claude
2026-09-09 13:22:13 +00:00
parent 7269bae11c
commit 497859dc76
@@ -62,13 +62,22 @@ import androidx.compose.ui.window.Dialog
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.search.QuerySerializer
import com.vitorpamplona.amethyst.commons.search.UserSearchEngine
import com.vitorpamplona.amethyst.commons.ui.search.SearchFieldState
import com.vitorpamplona.amethyst.commons.ui.search.TokenizedSearchField
import com.vitorpamplona.amethyst.commons.ui.search.rememberChipNames
import com.vitorpamplona.amethyst.commons.ui.search.rememberPersonCandidates
import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.amethyst.desktop.search.DesktopRelayUserSearchDelegate
import com.vitorpamplona.amethyst.desktop.ui.relay.LocalRelayCategories
import com.vitorpamplona.amethyst.desktop.ui.theme.hoverHighlight
@Composable
fun SearchSpotlight(
localCache: DesktopLocalCache,
relayManager: DesktopRelayConnectionManager,
onSelectProfile: (String) -> Unit,
onSelectNote: (String) -> Unit,
onSelectHashtag: (String) -> Unit,
@@ -79,6 +88,19 @@ fun SearchSpotlight(
val focusRequester = remember { FocusRequester() }
val fieldState = remember { SearchFieldState() }
// The people a half-written `from:`/`to:` token offers, and the names their finished
// chips draw. Same engine the full search screen uses: cache hits first, then whatever
// the account's search relays answer with.
val searchRelays by LocalRelayCategories.current.searchRelays.collectAsState()
val userSearch =
remember(relayManager, localCache) {
UserSearchEngine(localCache, scope).apply {
relayDelegate = DesktopRelayUserSearchDelegate(relayManager, localCache, { searchRelays }, scope)
}
}
val personCandidates = rememberPersonCandidates(userSearch)
val chipNames = rememberChipNames(userSearch)
val history by SearchHistoryStore.history.collectAsState()
val savedSearches by SearchHistoryStore.savedSearches.collectAsState()
@@ -130,7 +152,11 @@ fun SearchSpotlight(
) {
// Search input
Row(
verticalAlignment = Alignment.CenterVertically,
// Top, not centre: the picker opens *below* the field inside the
// same column, and centring would drag the leading icon halfway
// down it. With no picker up the two are the same height, so the
// resting layout is unchanged.
verticalAlignment = Alignment.Top,
modifier = Modifier.fillMaxWidth().padding(16.dp),
) {
Icon(
@@ -148,6 +174,9 @@ fun SearchSpotlight(
modifier = Modifier.weight(1f),
fieldModifier = Modifier.focusRequester(focusRequester),
placeholder = "Search notes, profiles, hashtags...",
people = personCandidates,
displayName = chipNames,
onPeopleQuery = { userSearch.search(it) },
textStyle = MaterialTheme.typography.bodyLarge,
onSubmit = { onOpenFullSearch(fieldState.text) },
)