From b402bf4b3eee95af031c2e267369b350c6625a19 Mon Sep 17 00:00:00 2001 From: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:06:06 +0000 Subject: [PATCH] android, desktop: hide the banners and cards above the chats while online search results are shown While a "Search in Directory" or Search key search runs or shows results, the chat list leaves out its banner slot, the reachable toolbar card and the connect cards. Typing alone, which only filters the chats, keeps them, and they come back when the results go. --- .../simplex/common/views/chatlist/ChatListView.kt | 8 +++++--- apps/multiplatform/product/views/chat-list.md | 8 +++++--- apps/multiplatform/spec/client/chat-list.md | 12 ++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 5091ba257d..ad454d3aed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1138,14 +1138,16 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat ChatListSearchBarWithTags(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate, onlineSearch) } } - if (!oneHandUICardShown.value) { + if (!oneHandUICardShown.value && !onlineSearch.showResults) { item { ToggleChatListCard() } } // one slot: a badge the user paid for ending outranks the pitch to get one val alert = BadgeModel.alert.value - if (supportEnded() && alert != null) { + if (onlineSearch.showResults) { + // banners give way to online results + } else if (supportEnded() && alert != null) { item { SideEffect { chatModel.chatListBanner = ChatListBanner.BadgeExpired } Box(Modifier.zIndex(1f).padding(16.dp)) { @@ -1204,7 +1206,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } nameRows(rows.names, oneHandUI.value) directoryRows(rows, onlineSearch, oneHandUI.value) - if (!addressCreationCardShown.value) { + if (!addressCreationCardShown.value && !onlineSearch.showResults) { item { ChatListFeatureCards() } diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md index 9242964223..8100963e91 100644 --- a/apps/multiplatform/product/views/chat-list.md +++ b/apps/multiplatform/product/views/chat-list.md @@ -68,6 +68,8 @@ Searching beyond the user's own chats happens only when the user asks, and the r A result is not shown twice: a name or directory entry for a chat already shown in the list is hidden, and a directory entry is hidden when the name search found the same channel (matched by its link). A group the user joined through a link other than the directory's still appears in the Directory part. Directory entries that are not group or channel links are ignored. Looking up a name never shows an error; a name that is not found simply gives no row, and "Connect to …" stays. +While an online search runs or has results, the list shows no banner, no one-hand UI card and no connect cards. Typing alone, which only filters the chats, keeps them; they come back when the results go (the text changes or is cleared) or when a search is cancelled before showing anything. + With no conversations yet (onboarding), the search bar is shown above the onboarding pages (below them with the one-hand UI) on Android, and the online results replace the pages while a search runs or has results. On desktop the onboarding pages are in the centre pane, so the chat list pane shows the search bar and the results only. Once a result is joined and the onboarding state ends, the search is cleared. ### Chat Filter Tags (`TagsView`) @@ -130,10 +132,10 @@ Each chat type provides specific dropdown menu items: | Element | Condition | Description | |---|---|---| -| One-hand UI card (`ToggleChatListCard`) | `oneHandUICardShown == false` | Dismissible card introducing bottom toolbar mode with toggle switch | -| Address creation card (`AddressCreationCard`) | `addressCreationCardShown == false` | Prompts user to create a SimpleX address; tappable card opens `UserAddressLearnMore` | +| One-hand UI card (`ToggleChatListCard`) | `oneHandUICardShown == false`, and no online search results shown | Dismissible card introducing bottom toolbar mode with toggle switch | +| Connect cards (`ChatListFeatureCards`, `ConnectBannerCard`) | `addressCreationCardShown == false`, the user has conversations, and no online search results shown | Two cards, "New 1-time link" and "Paste link" ("Paste link / Scan" on Android), each opening `NewChatView`; the X sets `addressCreationCardShown` | | FAB (new chat button) | Standard mode, search empty, chat running | `FloatingActionButton` at bottom-right, pencil icon, opens `NewChatSheet` | -| Crowdfunding banner (`GetStakeBanner`) | `crowdfundingAvailable()` and not dismissed | Gradient card above the chats, and below the onboarding cards when there are no conversations; opens `GetStakeView`. The dismiss X appears once the banner has been tapped and there is at least one chat; dismissing hides it until hints are reset | +| Crowdfunding banner (`GetStakeBanner`) | `crowdfundingAvailable()`, not dismissed, and, in the chat list, no online search results shown | Gradient card above the chats, and below the onboarding cards when there are no conversations; opens `GetStakeView`. The dismiss X appears once the banner has been tapped and there is at least one chat; dismissing hides it until hints are reset | ### Empty States diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md index a0b2a2e677..5bd4ffeae9 100644 --- a/apps/multiplatform/spec/client/chat-list.md +++ b/apps/multiplatform/spec/client/chat-list.md @@ -227,7 +227,7 @@ On desktop, the currently selected chat (`chatModel.chatId.value == chat.id`) re ### TagsView -**Location:** [`ChatListView.kt#L1450`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1450) +**Location:** [`ChatListView.kt#L1452`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1452) Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable). @@ -340,7 +340,7 @@ Gradient card inviting the user to invest on Wefunder. Shown only when `crowdfun | Chat list | in `ChatList`'s `LazyColumn`, after `ToggleChatListCard` and before the chats | `Box(Modifier.zIndex(1f).padding(16.dp))` | | Onboarding | inside `ConnectOnboardingView` (`views/newchat/OnboardingCards.kt`), below the pager, so it shares the pages' width limit on desktop and their dimming while a start modal is open; opens the page in `ModalManager.center` on desktop, `ModalManager.start` on Android | `padding(start/end = DEFAULT_PADDING, bottom = 8.dp)`, in a `Column` where the pager takes `weight(1f)` | -The list has a single banner slot, filled by an `if`/`else if` chain in priority order: the support-ended alert (`supportEnded()`), the renewal-failure alert (`badgeIssueFailed()`), the pitch, then the Wefunder banner. Each banner's `item` records itself in `ChatModel.chatListBanner` (`BadgeExpired`, `BadgeIssueFailed`, `BadgePitch`, `GetStake`) in a `SideEffect`, and the pitch and Wefunder conditions start with `chatModel.bannerSlotFree(banner)` — true only while nothing else was shown this app session — so dismissing a banner never puts another in its place until restart. The alerts have no such check: an alert takes the slot whenever present, and once shown it holds it. The pitch also requires `noShownBadge()` (`views/newchat/OnboardingCards.kt`), false until `BadgeModel.isCurrent` for the current user, so it cannot take the slot from a supporter whose badge loads a moment later. `ConnectOnboardingView` applies the same `bannerSlotFree` check and records `GetStake`. +The list has a single banner slot, filled by an `if`/`else if` chain in priority order: the support-ended alert (`supportEnded()`), the renewal-failure alert (`badgeIssueFailed()`), the pitch, then the Wefunder banner. Each banner's `item` records itself in `ChatModel.chatListBanner` (`BadgeExpired`, `BadgeIssueFailed`, `BadgePitch`, `GetStake`) in a `SideEffect`, and the pitch and Wefunder conditions start with `chatModel.bannerSlotFree(banner)` — true only while nothing else was shown this app session — so dismissing a banner never puts another in its place until restart. The alerts have no such check: an alert takes the slot whenever present, and once shown it holds it. The pitch also requires `noShownBadge()` (`views/newchat/OnboardingCards.kt`), false until `BadgeModel.isCurrent` for the current user, so it cannot take the slot from a supporter whose badge loads a moment later. `ConnectOnboardingView` applies the same `bannerSlotFree` check and records `GetStake`. While online search results are shown (`onlineSearch.showResults`, after "Search in Directory" or the Search key) the slot stays empty, and `ToggleChatListCard` and `ChatListFeatureCards` are left out too; typing alone, which only filters the chats, keeps them, and they return when the search is cleared or cancelled before showing anything. `crowdfundingAvailable()` launches an effect to load the store country, so both call sites read it in the composable body rather than inside the `LazyColumn` builder. @@ -384,7 +384,7 @@ The search text can be sent online on request: to SimpleX Directory, to find pub Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link ([`searchTrimmed`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1006-L1007), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L328-L334), which runs the search at once when `appPrefs.directorySearchAlertShown` is set and otherwise shows the three-button alert ([`showDirectorySearchAlert`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L336-L364), `showAlertDialogButtonsColumn` with `parseHtml = false` so the paragraph break shows): Search / Search and don't show again / Cancel. Its message names both recipients: the directory, and, for a SimpleX name, the server that resolves names. The preference is in `hintPreferences`, so "Reset all hints" brings the alert back. Each trigger launches the model method with `withApi`. -The rows next to the search field share one slot with the tags, laid out by [`TagsOrConnectByName`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1319-L1373) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1240-L1265): +The rows next to the search field share one slot with the tags, laid out by [`TagsOrConnectByName`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1321-L1375) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1242-L1267): | Search text | Android, one-hand UI (slot above the field) | Android, top bar (slot below the field) | Desktop (either mode) | |-------------|-----------------------------------|------------------------------|------| @@ -445,7 +445,7 @@ For each new entry of a page (not already shown, not repeated in the page), the ### Name resolution -[`resolveNames(name, gen)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L142-L158) resolves `[name]` for a candidate starting with `@` or `#`, else `["@$name", "#$name"]` (the candidate comes from [`nameSearchCandidate`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1282-L1293), as on the typing path). Each target is planned with `apiConnectPlan(rh, target, resolveMode = PRMUnknown, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)`; it stops before a target once the generation changed, so a cancelled or replaced search sends no further names. [`NameSearchResult(target, result)`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L112-L171) (a private constructor behind a companion `operator fun invoke` that returns null for no row) maps the plan with one exhaustive `when`: a contact address or group link gives a row (profile from the link data or the known chat), the user's own contact address, an invitation link and an error give none. `simplexName` is the plan's `planSimplexName?.shortStr`, `localChatId` comes from `planLocalChatId`, `iconName` is an `ImageResource`, and for a group link `resolvedLink` is the link the core resolved (`connLink.connShortLink`). Failures are logged at debug level and never shown as alerts; the false `inProgress` also keeps `sendCmdWithRetry` from showing its network retry alert. +[`resolveNames(name, gen)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L142-L158) resolves `[name]` for a candidate starting with `@` or `#`, else `["@$name", "#$name"]` (the candidate comes from [`nameSearchCandidate`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1284-L1295), as on the typing path). Each target is planned with `apiConnectPlan(rh, target, resolveMode = PRMUnknown, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)`; it stops before a target once the generation changed, so a cancelled or replaced search sends no further names. [`NameSearchResult(target, result)`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L112-L171) (a private constructor behind a companion `operator fun invoke` that returns null for no row) maps the plan with one exhaustive `when`: a contact address or group link gives a row (profile from the link data or the known chat), the user's own contact address, an invitation link and an error give none. `simplexName` is the plan's `planSimplexName?.shortStr`, `localChatId` comes from `planLocalChatId`, `iconName` is an `ImageResource`, and for a group link `resolvedLink` is the link the core resolved (`connLink.connShortLink`). Failures are logged at debug level and never shown as alerts; the false `inProgress` also keeps `sendCmdWithRetry` from showing its network retry alert. ### De-duplication @@ -463,10 +463,10 @@ Only store-derived chat ids and the link the core resolved can hide a row, never | Where | Rows | Empty state | |-------|------|-------------| -| [`ChatList`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1066-L1238) | `rows(shownChatIds)` with the ids of the shown chats ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1109)); after the local chats and before `ChatListFeatureCards`, [`nameRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L517-L532) then [`directoryRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L534-L554) | `NoChatsView` overlay only when no chat is shown, the model is not `searching` and `rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1216-L1221)); the `you_have_no_chats` overlay of `ChatListWithLoadingScreen` (no chats at all) is hidden while `showResults` | +| [`ChatList`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1066-L1240) | `rows(shownChatIds)` with the ids of the shown chats ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1109)); after the local chats and before `ChatListFeatureCards`, [`nameRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L517-L532) then [`directoryRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L534-L554) | `NoChatsView` overlay only when no chat is shown, the model is not `searching` and `rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1218-L1223)); the `you_have_no_chats` overlay of `ChatListWithLoadingScreen` (no chats at all) is hidden while `showResults` | | Onboarding branch of [`ChatListWithLoadingScreen`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L459-L506) | A `Column` with `ChatListSearchBarWithTags` first (top bar) or last (one-hand UI), and between them a `Box(Modifier.weight(1f))` holding, while `showResults`, a `LazyColumnWithScrollBarNoAppBar` of the same parts with `shownChatIds = emptySet()` (reversed in one-hand mode, `imePadding()` in top-bar mode), otherwise `ConnectOnboardingView()` on Android | A centred "No chats found" item when not searching and `rows.isEmpty` | -The onboarding `Column` takes the toolbar and navigation-bar paddings and consumes the navigation-bar insets, so the results list's `imePadding()` does not count them twice. On desktop the onboarding pages are drawn in the centre pane, so the chat list pane shows only the search bar and the results. When a joined result adds a chat and the onboarding state ends, `ChatList` enters composition and its `LaunchedEffect(activeFilter.value)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1235-L1237)) clears the search text, which resets the model; the iOS app keeps the search there. +The onboarding `Column` takes the toolbar and navigation-bar paddings and consumes the navigation-bar insets, so the results list's `imePadding()` does not count them twice. On desktop the onboarding pages are drawn in the centre pane, so the chat list pane shows only the search bar and the results. When a joined result adds a chat and the onboarding state ends, `ChatList` enters composition and its `LaunchedEffect(activeFilter.value)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1237-L1239)) clears the search text, which resets the model; the iOS app keeps the search there. Each part renders only when it has rows: the Names part when `rows.names` is not empty, the Directory part when `rows.directory` is not empty or `rows.footer` is set. `nameRows` and `directoryRows` are `LazyListScope` builders; each part has a header `item` ([`SearchPartHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L567-L575): `body2`, secondary colour, `semantics { heading() }`) rather than a `stickyHeader`, and its rows are `items` keyed by `id`. [`searchPart`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L556-L565) emits the header before the part's rows with the top bar and after them in the one-hand UI, so it is drawn above them in both layouts, the one-hand list being reversed. A lazy list keeps its first visible item in place by key, so in the reversed onboarding list, whose first item is then a keyed row, names arriving after the directory page can end up out of view: that list scrolls to its first item when name results arrive in one-hand mode ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L474-L480)). The `ChatList` branch starts with a keyless spacer, which stays the anchor.