From 28e8fc627e1b1768a1dc6d648e556fc4fc77a354 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 28 May 2026 13:00:40 +0300 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20polished=20inline=20search=20s?= =?UTF-8?q?tates=20=E2=80=94=20loading,=20empty,=20streaming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LinearProgressIndicator at top of expanded card (animated in/out) - Loading state: centered icon + "Searching N relays..." - Empty state: "No results found" / "No search relays configured" - Results stream in incrementally from relays - 1s debounce for relay queries, save to search history (no dupes) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../amethyst/desktop/ui/FeedScreen.kt | 99 ++++++++++++------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 2063e9594e..033f38bf93 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -52,6 +52,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -1058,60 +1059,84 @@ private fun FeedTabsHeader( exit = shrinkVertically(animationSpec = tween(150)) + fadeOut(tween(100)), ) { Column { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - val hasQuery = searchText.text.isNotBlank() val isSearching by searchState.isSearching.collectAsState() val people by searchState.peopleResults.collectAsState() val notes by searchState.noteResults.collectAsState() val hasResults = people.isNotEmpty() || notes.isNotEmpty() + // Linear progress bar at the top — visible while searching + AnimatedVisibility( + visible = isSearching || (hasQuery && !hasResults), + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + if (hasQuery) { - if (isSearching && !hasResults) { - // Loading state while waiting for relay results - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + // Results stream in as they arrive + if (hasResults) { + SearchResultsList( + state = searchState, + onNavigateToProfile = { pubkey -> + onSearchExpandedChange(false) + onNavigateToProfile(pubkey) + }, + onNavigateToThread = { noteId -> + onSearchExpandedChange(false) + onNavigateToThread(noteId) + }, + localCache = localCache, + modifier = Modifier.heightIn(max = 400.dp).fillMaxWidth(), + ) + } else if (isSearching) { + // Loading — centered in results area + Column( + modifier = Modifier.fillMaxWidth().padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, + Icon( + MaterialSymbols.Search, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.height(8.dp)) Text( "Searching ${searchRelays.size} relays...", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + } else { + // Search complete, no results + Column( + modifier = Modifier.fillMaxWidth().padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + MaterialSymbols.Search, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + ) + Spacer(Modifier.height(8.dp)) + Text( + if (searchRelays.isEmpty()) "No search relays configured" else "No results found", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - // Show search results (reuses SearchResultsList) - SearchResultsList( - state = searchState, - onNavigateToProfile = { pubkey -> - onSearchExpandedChange(false) - onNavigateToProfile(pubkey) - }, - onNavigateToThread = { noteId -> - onSearchExpandedChange(false) - onNavigateToThread(noteId) - }, - localCache = localCache, - modifier = Modifier.heightIn(max = 400.dp).fillMaxWidth(), - ) - - if (!isSearching && !hasResults && searchRelays.isEmpty()) { - Text( - "No search relays configured", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp), - ) - } - - // "Open full search" link + // "Open full search" — always visible when there's a query HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),