From 497859dc76c03c3538eb24a7d940d52108e7e2d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 13:22:13 +0000 Subject: [PATCH] feat(search): give the desktop spotlight its people picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_017yKjw2WqwZpSzsqcYZMnkV --- .../desktop/ui/search/SearchSpotlight.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSpotlight.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSpotlight.kt index beb8012647..be707dc14d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSpotlight.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSpotlight.kt @@ -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) }, )