diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 9331ce085d..d31173436d 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -236,6 +236,13 @@ struct ChatListView: View { } } } + // here, not in the search bar: a profile switch can replace the search bar, and the new one never sees the change + .onChange(of: searchText) { _ in + onlineSearch.reset() + } + .onChange(of: chatModel.currentUser?.userId) { _ in + onlineSearch.reset() + } .environmentObject(chatTagsModel) } @@ -911,12 +918,6 @@ struct ChatListSearchBar: View { .onChange(of: searchFocussed) { sf in withAnimation { searchMode = sf } } - .onChange(of: searchText) { _ in - onlineSearch.reset() - } - .onChange(of: m.currentUser?.userId) { _ in - onlineSearch.reset() - } .onChange(of: searchText) { t in if ignoreSearchTextChange { ignoreSearchTextChange = false diff --git a/apps/ios/Shared/Views/ChatList/DirectorySearchView.swift b/apps/ios/Shared/Views/ChatList/DirectorySearchView.swift index 4aaf4b5058..525b20d228 100644 --- a/apps/ios/Shared/Views/ChatList/DirectorySearchView.swift +++ b/apps/ios/Shared/Views/ChatList/DirectorySearchView.swift @@ -250,7 +250,7 @@ struct DirectoryLoadMoreRow: View { var body: some View { Button { - Task { await onlineSearch.loadMore() } + confirmDirectorySearch { Task { await onlineSearch.loadMore() } } } label: { HStack(spacing: 4) { Image(systemName: "plus.magnifyingglass") @@ -276,7 +276,7 @@ struct DirectoryRetryRow: View { var body: some View { Button { - Task { await onlineSearch.retry() } + confirmDirectorySearch { Task { await onlineSearch.retry() } } } label: { HStack(spacing: 4) { Image(systemName: "arrow.clockwise") @@ -329,7 +329,7 @@ func confirmDirectorySearch(_ search: @escaping () -> Void) { private func showDirectorySearchAlert(onSearch: @escaping () -> Void) { showAlert( NSLocalizedString("Enable Directory search?", comment: "alert title"), - message: NSLocalizedString("Search text is sent to SimpleX Directory, each search separately and without your profile, so it can't build your search history.", comment: "alert message"), + message: NSLocalizedString("Search text is sent to SimpleX Directory, each search separately and without your profile.", comment: "alert message"), actions: {[ UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .cancel), UIAlertAction( diff --git a/apps/ios/product/rules.md b/apps/ios/product/rules.md index f26145ebe1..03856888cf 100644 --- a/apps/ios/product/rules.md +++ b/apps/ios/product/rules.md @@ -153,7 +153,7 @@ ### RULE-24: Online results are never saved **Rule:** Directory results MUST NOT be stored, and MUST live only as long as the search text and the active profile: any text change or profile switch clears them, and a reply that arrives after that (or after the search was cancelled) MUST NOT be shown. -**Enforced by:** `DirectorySearchView.swift` (`OnlineSearchModel.reset()` bumps `generation`; every publish checks it); `ChatListView.swift` (`ChatListSearchBar` resets on `searchText` and `currentUser?.userId` changes); `NewChatMenuButton.swift` (`NewChatSheet` resets on the same changes). +**Enforced by:** `DirectorySearchView.swift` (`OnlineSearchModel.reset()` bumps `generation`; every publish checks it); `ChatListView.swift` (`ChatListView`, which owns the search text and the model, resets on `searchText` and `currentUser?.userId` changes); `NewChatMenuButton.swift` (`NewChatSheet` resets on the same changes). **Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) ### RULE-25: Only group and channel links are accepted from the directory @@ -173,5 +173,5 @@ ### RULE-28: Consent before the first online search **Rule:** The search text MUST NOT be sent online while the "Directory search" switch in Privacy settings is off, unless the user confirms the "Enable Directory search?" alert, which says the text goes to SimpleX Directory and turns the switch on. Cancel MUST send nothing. The search never resolves SimpleX names, so the directory is its only recipient. -**Enforced by:** `DirectorySearchView.swift` (`confirmDirectorySearch`, `DEFAULT_PRIVACY_DIRECTORY_SEARCH`); `PrivacySettings.swift` (the switch); both triggers in `ChatListView.swift` and `NewChatMenuButton.swift` go through it. +**Enforced by:** `DirectorySearchView.swift` (`confirmDirectorySearch`, `DEFAULT_PRIVACY_DIRECTORY_SEARCH`); `PrivacySettings.swift` (the switch); both triggers in `ChatListView.swift` and `NewChatMenuButton.swift` go through it, and so do the load and retry rows (`DirectoryLoadMoreRow`, `DirectoryRetryRow`). **Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) diff --git a/apps/ios/product/views/chat-list.md b/apps/ios/product/views/chat-list.md index 3ae27eb842..9da751484e 100644 --- a/apps/ios/product/views/chat-list.md +++ b/apps/ios/product/views/chat-list.md @@ -46,7 +46,7 @@ Searching beyond the user's own chats happens only when the user asks, and the r | Keyboard Search key | The same directory search. It never looks up SimpleX names | | "Connect to …" row | Shown for a SimpleX name (e.g. `alice` or `#news.testing`), and stays next to the results; it is the only way to look up a name | | "Directory search" switch | In Settings › Privacy › Chats, after "Send link previews"; off by default | -| "Enable Directory search?" alert | Every search asks while the switch is off: search text is sent to SimpleX Directory, each search separately and without the user's profile, so it can't build a search history. Cancel sends nothing; Enable turns the switch on and searches. Once it is on, searches don't ask | +| "Enable Directory search?" alert | Every search asks while the switch is off: search text is sent to SimpleX Directory, each search separately and without the user's profile. Cancel sends nothing; Enable turns the switch on and searches. Once it is on, searches don't ask | | Results | One list without headers below the local chats. First your chats among the results that the list above doesn't show (for example because the name doesn't match the typed text), as their regular chat rows; then the other results, with the same avatar size as chat rows: avatar, `#` name, the directory's SimpleX name, description, then the member or subscriber count and "Open" on the right. A join still in progress without a chat shows "Already joining" in place of the count and no "Open" | | Open | Tapping a result (or its "Open") loads the group's or channel's profile, with the spinner in the search field, and opens the chat straight away, with its Join button and no "Open channel?" question; a channel's banner shows its subscriber count. The user joins from there. If the link's signature was checked or its name also leads to another chat, the question shows first, as for a pasted link, with the signature result or the other chat's button. A result that is already joining shows the same sheet or alert as pasting its link would | | Long press | On iOS 16 and later, a long press on a result shows a menu with Open, previewing the row; iOS 15 has no menu | diff --git a/apps/ios/spec/client/chat-list.md b/apps/ios/spec/client/chat-list.md index ad5c0540e4..a11e2dc86d 100644 --- a/apps/ios/spec/client/chat-list.md +++ b/apps/ios/spec/client/chat-list.md @@ -324,7 +324,7 @@ Tapping the card opens [`GetStakeView`](../../Shared/Views/Onboarding/WhatsNewVi **Files**: `Shared/Views/ChatList/DirectorySearchView.swift` (model and rows), `Shared/Model/DirectorySearch.swift` (response and result types), `Shared/Views/ChatList/ChatListView.swift` (triggers and rendering) -The search text can be sent online on request to SimpleX Directory, to find public groups and channels. Results are shown below the local chats as one list without headers: your chats first, then the other results. They are never saved and live only as long as the search text and the active profile: [`ChatListSearchBar`](../../Shared/Views/ChatList/ChatListView.swift#L823) calls `reset()` on every change of `searchText` ([L921](../../Shared/Views/ChatList/ChatListView.swift#L914)) and of `currentUser?.userId` ([L924](../../Shared/Views/ChatList/ChatListView.swift#L917)). The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#12-new-chat-online-search)). +The search text can be sent online on request to SimpleX Directory, to find public groups and channels. Results are shown below the local chats as one list without headers: your chats first, then the other results. They are never saved and live only as long as the search text and the active profile: [`ChatListView`](../../Shared/Views/ChatList/ChatListView.swift#L154), which owns `searchText` and the model, calls `reset()` from `viewBody`, which is shown in every state, on every change of `searchText` ([L240](../../Shared/Views/ChatList/ChatListView.swift#L240)) and of `currentUser?.userId` ([L243](../../Shared/Views/ChatList/ChatListView.swift#L243)). The search bar is a different instance in the onboarding branch and in the list, so a profile switch that also switches between them would replace the bar before it could see the change. The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#12-new-chat-online-search)). ### Triggers @@ -333,7 +333,7 @@ The search text can be sent online on request to SimpleX Directory, to find publ | Keyboard Search key (`.submitLabel(.search)`) | [`runDirectorySearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1019) | `searchDirectory(text)` | | "Search in Directory" row ([`SearchInDirectoryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L163)) | [`runDirectorySearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1019) | `searchDirectory(text)` | -Both triggers run the same directory search, which never resolves SimpleX names: a name is looked up only by "Connect to …" (`ConnectByNameRow`). The field neither autocorrects nor capitalizes, so the Search key searches what was typed. Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link, and both go through [`confirmDirectorySearch()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L321), which runs the search at once when the "Directory search" switch is on (`DEFAULT_PRIVACY_DIRECTORY_SEARCH`, default false, in Privacy settings › Chats after "Send link previews") and otherwise shows the alert [`showDirectorySearchAlert`](../../Shared/Views/ChatList/DirectorySearchView.swift#L329): "Enable Directory search?", with the message "Search text is sent to SimpleX Directory, each search separately and without your profile, so it can't build your search history." and two actions, Cancel (sends nothing) and Enable (turns the switch on, then searches). So every search asks while the switch is off. The key is not in `hintDefaults`: "Reset all hints" leaves the switch as it is. +Both triggers run the same directory search, which never resolves SimpleX names: a name is looked up only by "Connect to …" (`ConnectByNameRow`). The field neither autocorrects nor capitalizes, so the Search key searches what was typed. Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link, and both go through [`confirmDirectorySearch()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L321), which runs the search at once when the "Directory search" switch is on (`DEFAULT_PRIVACY_DIRECTORY_SEARCH`, default false, in Privacy settings › Chats after "Send link previews") and otherwise shows the alert [`showDirectorySearchAlert`](../../Shared/Views/ChatList/DirectorySearchView.swift#L329): "Enable Directory search?", with the message "Search text is sent to SimpleX Directory, each search separately and without your profile." and two actions, Cancel (sends nothing) and Enable (turns the switch on, then searches). So every search asks while the switch is off. The key is not in `hintDefaults`: "Reset all hints" leaves the switch as it is. The rows above or below the search field follow the toolbar mode. "Search in Directory" hides while results for the current text show or load (`!onlineSearch.showResults`); "Connect to …" stays next to them: @@ -421,8 +421,8 @@ So nothing is shown twice: a result that is a chat the list shows is hidden, a c |-----|------|-----| | Your chat | Each loaded chat of `rows.yourChats`, drawn with the chat list's own row, [`chatListNavLink(_:)`](../../Shared/Views/ChatList/ChatListView.swift#L637) (`ChatListNavLink` with the local chats' modifiers) | as a local chat: opens it | | Result | [`DirectorySearchRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L185) with `imageSize: dynamicSize(userFont).profileImageSize`, the chat rows' avatar size (63 pt at the default text size): avatar, `#` + name, the directory's SimpleX name claim as plain text, description (2 lines), then the caption: the member or subscriber count, or "Already joining" for `.joining` (secondary colour); trailing "Open" (primary colour, one line, fixed size), except for `.joining` | [`openResult(_:)`](../../Shared/Views/ChatList/ChatListView.swift#L503): `openDirectoryResult(result, dismiss: false)`, `cleanup`: `refreshStates()` (see Opening a result) | -| Load more | [`DirectoryLoadMoreRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L247) via [`directoryLoadMoreRow()`](../../Shared/Views/ChatList/ChatListView.swift#L655): `plus.magnifyingglass` and "Load more · %d left" when `moreCount` is above 0, else "Load more"; disabled and in the secondary colour while `loading` | `loadMore()` | -| Retry | [`DirectoryRetryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L273) via [`directoryRetryRow()`](../../Shared/Views/ChatList/ChatListView.swift#L661): `arrow.clockwise` and "Search failed, tap to retry" | `retry()` | +| Load more | [`DirectoryLoadMoreRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L247) via [`directoryLoadMoreRow()`](../../Shared/Views/ChatList/ChatListView.swift#L655): `plus.magnifyingglass` and "Load more · %d left" when `moreCount` is above 0, else "Load more"; disabled and in the secondary colour while `loading` | `loadMore()`, through `confirmDirectorySearch` | +| Retry | [`DirectoryRetryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L273) via [`directoryRetryRow()`](../../Shared/Views/ChatList/ChatListView.swift#L661): `arrow.clockwise` and "Search failed, tap to retry" | `retry()`, through `confirmDirectorySearch` | On iOS 16 and later a long press on a result shows a context menu, [`DirectoryResultMenu`](../../Shared/Views/ChatList/DirectorySearchView.swift#L290), with one action, Open (`message` icon), which does what a tap does. Its preview is the row without its Open, padded, the screen width less 32 pt, on the theme background. The modifier is applied before the row's flip, so in the one-hand UI the preview is drawn upright (checked in the iOS simulator tests). iOS 15 has no menu; a tap still opens. 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 ba30c2db56..9940b0cf43 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 @@ -244,6 +244,13 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow>(emptySet()) } val connectNameCandidate = remember { mutableStateOf(null) } val onlineSearch = remember { OnlineSearchModel() } + // here, not in the search bar: a profile switch can replace the search bar, and the new one never sees the change + KeyChangeEffect(searchText.value.text) { + onlineSearch.reset() + } + KeyChangeEffect(chatModel.currentUser.value?.userId) { + onlineSearch.reset() + } Box(Modifier.fillMaxSize()) { if (oneHandUI.value) { ChatListWithLoadingScreen(searchText, listState, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate, onlineSearch) @@ -904,12 +911,6 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState focusManager.clearFocus() } } - KeyChangeEffect(searchText.value.text) { - onlineSearch.reset() - } - KeyChangeEffect(chatModel.currentUser.value?.userId) { - onlineSearch.reset() - } val view = LocalMultiplatformView() LaunchedEffect(Unit) { snapshotFlow { searchText.value.text } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt index dbd77ff1a3..5aee3205a7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt @@ -262,7 +262,7 @@ fun DirectoryLoadMoreRow(onlineSearch: OnlineSearchModel) { Modifier .fillMaxWidth() .clickable(enabled = !onlineSearch.loading) { - withApi { onlineSearch.loadMore() } + confirmDirectorySearch { withApi { onlineSearch.loadMore() } } } .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically @@ -282,7 +282,7 @@ fun DirectoryRetryRow(onlineSearch: OnlineSearchModel) { Modifier .fillMaxWidth() .clickable { - withApi { onlineSearch.retry() } + confirmDirectorySearch { withApi { onlineSearch.retry() } } } .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically diff --git a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml index 5b30e62fdc..26cc2a06a0 100644 --- a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml @@ -516,7 +516,7 @@ Search or paste SimpleX link Search in Directory Enable Directory search? - Search text is sent to SimpleX Directory, each search separately and without your profile, so it can\'t build your search history. + Search text is sent to SimpleX Directory, each search separately and without your profile. Enable %1$d members Already joining diff --git a/apps/multiplatform/product/rules.md b/apps/multiplatform/product/rules.md index 38d84dbb15..ed80b60a66 100644 --- a/apps/multiplatform/product/rules.md +++ b/apps/multiplatform/product/rules.md @@ -261,11 +261,11 @@ This document specifies invariants enforced by the Android and Desktop (Kotlin/C **Invariant:** Directory results MUST NOT be stored, and MUST live only as long as the search text and the active profile: any text change or profile switch clears them, and a reply that arrives after that (or after the search was cancelled) MUST NOT be shown. -**Enforcement:** `OnlineSearchModel.reset()` bumps `generation`, and every publish in `request` and `refreshStates` checks it; `cancelRequest()` bumps it too. `ChatListSearchBar` and `NewChatSheetLayout` call `reset()` from `KeyChangeEffect`s on the search text and on `chatModel.currentUser.value?.userId`. The model is kept in Compose `remember` only, so it is also dropped when its screen leaves composition (e.g. an Android rotation). +**Enforcement:** `OnlineSearchModel.reset()` bumps `generation`, and every publish in `request` and `refreshStates` checks it; `cancelRequest()` bumps it too. `ChatListView` (which owns the search text and the model, not the search bar) and `NewChatSheetLayout` call `reset()` from `KeyChangeEffect`s on the search text and on `chatModel.currentUser.value?.userId`. The model is kept in Compose `remember` only, so it is also dropped when its screen leaves composition (e.g. an Android rotation). **Location:** - `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `OnlineSearchModel` -- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt` -- `ChatListSearchBar` +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt` -- `ChatListView` - `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt` -- `NewChatSheetLayout` **Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) @@ -317,10 +317,10 @@ This document specifies invariants enforced by the Android and Desktop (Kotlin/C **Invariant:** The search text MUST NOT be sent online while the "Directory search" switch in Privacy settings is off, unless the user confirms the "Enable Directory search?" alert, which says the text goes to SimpleX Directory and turns the switch on. Cancel MUST send nothing. The search never resolves SimpleX names, so the directory is its only recipient. -**Enforcement:** Both triggers (the "Search in Directory" row and the Search key / Enter) on the chat list and in New chat go through `confirmDirectorySearch()`, which runs the search at once only when `AppPreferences.privacyDirectorySearch` is on and otherwise shows the alert; Enable sets the preference, then searches. The preference is a setting, not a hint, so "Reset all hints" leaves it as it is. +**Enforcement:** Both triggers (the "Search in Directory" row and the Search key / Enter) on the chat list and in New chat go through `confirmDirectorySearch()`, and so do the load and retry rows (`DirectoryLoadMoreRow`, `DirectoryRetryRow`), shared by both screens; it runs the search at once only when `AppPreferences.privacyDirectorySearch` is on and otherwise shows the alert; Enable sets the preference, then searches. The preference is a setting, not a hint, so "Reset all hints" leaves it as it is. **Location:** -- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `confirmDirectorySearch()`, `showDirectorySearchAlert()` +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `confirmDirectorySearch()`, `showDirectorySearchAlert()`, `DirectoryLoadMoreRow()`, `DirectoryRetryRow()` - `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `AppPreferences.privacyDirectorySearch` - `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt` -- the switch diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md index a5507f4226..d5a92aa498 100644 --- a/apps/multiplatform/product/views/chat-list.md +++ b/apps/multiplatform/product/views/chat-list.md @@ -64,7 +64,7 @@ Searching beyond the user's own chats happens only when the user asks, and the r | Keyboard Search key | On Android the keyboard's Search action, on desktop Enter. The same directory search; it never looks up SimpleX names | | "Connect to …" row | Shown for a SimpleX name (e.g. `alice` or `#news.testing`; on Android with the top bar it takes the tags' place), and stays next to the results; it is the only way to look up a name | | "Directory search" switch | In Settings › Privacy › Chats, after "Send link previews"; off by default | -| "Enable Directory search?" alert | Every search asks while the switch is off: search text is sent to SimpleX Directory, each search separately and without the user's profile, so it can't build a search history. Enable turns the switch on and searches; Cancel sends nothing. Once it is on, searches don't ask | +| "Enable Directory search?" alert | Every search asks while the switch is off: search text is sent to SimpleX Directory, each search separately and without the user's profile. Enable turns the switch on and searches; Cancel sends nothing. Once it is on, searches don't ask | | Results | One list without headers below the local chats. First your chats among the results that the list above doesn't show (for example because the name doesn't match the typed text), as their regular chat rows; then the other results, laid out as chat rows with the same avatar size: avatar, `#` name, the directory's SimpleX name, description, then the member or subscriber count and "Open" on the right. A join still in progress without a chat shows "Already joining" in place of the count and no "Open" | | Open | Tapping a result loads the group's or channel's profile and opens the chat straight away, with its Join button and no "Open channel?" question; a channel's banner shows its subscriber count. The user joins from there. If the link's signature was checked or its name also leads to another chat, the question shows first, as for a pasted link, with the signature result or the other chat's button. A result that is already joining shows the same alert as pasting its link would | | Result menu | A long press (or a right click on desktop) on a result shows a menu with Open | diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md index 2c56896edd..2194c6cacc 100644 --- a/apps/multiplatform/spec/client/chat-list.md +++ b/apps/multiplatform/spec/client/chat-list.md @@ -374,7 +374,7 @@ Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage **Files:** `views/chatlist/DirectorySearchView.kt` (model and rows), `model/DirectorySearch.kt` (response and result types), `views/chatlist/ChatListView.kt` (triggers and rendering), `views/helpers/SearchTextField.kt` (Search key) -The search text can be sent online on request to SimpleX Directory, to find public groups and channels. Results are shown below the local chats as one list without headers: your chats first, then the other results. They are never saved and live only as long as the search text and the active profile: [`ChatListSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L863-L966) calls `reset()` from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L907-L912)). The model is a plain `remember` in [`ChatListView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L246), so an Android rotation (which recreates the Activity) drops the results while the `rememberSaveable` search text stays. The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)). +The search text can be sent online on request to SimpleX Directory, to find public groups and channels. Results are shown below the local chats as one list without headers: your chats first, then the other results. They are never saved and live only as long as the search text and the active profile: [`ChatListView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L218), which owns the search text and the model, calls `reset()` from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L248-L253)). The search bar is a different instance in the onboarding branch and in the list, and in the list a lazy item that leaves composition off screen, so a profile switch could replace or skip the bar before it saw the change. The model is a plain `remember` in [`ChatListView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L246), so an Android rotation (which recreates the Activity) drops the results while the `rememberSaveable` search text stays. The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)). ### Triggers @@ -385,7 +385,7 @@ The search text can be sent online on request to SimpleX Directory, to find publ `SearchTextField` handles the key per platform: on Android through `KeyboardActions(onSearch)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L116-L121)), on desktop through `onPreviewKeyEvent` for `Key.Enter` / `Key.NumPadEnter` on `KeyUp` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L95-L102)), so one key press runs the search once. Every caller keeps `ImeAction.Search`; a caller without `onSearch` gets no handler. The field neither autocorrects nor capitalizes (`KeyboardOptions(capitalization = None, autoCorrect = false)`), so the Search key searches what was typed; being shared, this reaches every `SearchTextField`, unlike iOS, which changed only the chat list and New chat fields. -Both triggers run the same directory search, which never resolves SimpleX names: a name is looked up only by "Connect to …" (`ConnectByNameRow`). Both do nothing for empty (trimmed) text or a pasted SimpleX link ([`searchTrimmed`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L981-L982), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L326-L332), which runs the search at once when the "Directory search" switch is on (`appPrefs.privacyDirectorySearch`, key "PrivacyDirectorySearch", default false, in Privacy settings › Chats after "Send link previews") and otherwise shows the alert [`showDirectorySearchAlert`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L334-L356) (`showAlertDialogButtonsColumn` with `parseHtml = false`): "Enable Directory search?", with the message "Search text is sent to SimpleX Directory, each search separately and without your profile, so it can't build your search history." and two buttons, Enable (turns the switch on, then searches) and Cancel (sends nothing), in that order, as this alert's column already placed Cancel last. So every search asks while the switch is off. The preference is not in `hintPreferences`: "Reset all hints" leaves the switch as it is. Each trigger launches the model method with `withApi`. +Both triggers run the same directory search, which never resolves SimpleX names: a name is looked up only by "Connect to …" (`ConnectByNameRow`). Both do nothing for empty (trimmed) text or a pasted SimpleX link ([`searchTrimmed`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L981-L982), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L326-L332), which runs the search at once when the "Directory search" switch is on (`appPrefs.privacyDirectorySearch`, key "PrivacyDirectorySearch", default false, in Privacy settings › Chats after "Send link previews") and otherwise shows the alert [`showDirectorySearchAlert`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L334-L356) (`showAlertDialogButtonsColumn` with `parseHtml = false`): "Enable Directory search?", with the message "Search text is sent to SimpleX Directory, each search separately and without your profile." and two buttons, Enable (turns the switch on, then searches) and Cancel (sends nothing), in that order, as this alert's column already placed Cancel last. So every search asks while the switch is off. The preference is not in `hintPreferences`: "Reset all hints" leaves the switch as it is. 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#L1291-L1347) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1208-L1237). "Search in Directory" hides while results for the current text show or load (`!onlineSearch.showResults`); "Connect to …" stays next to them: @@ -483,8 +483,8 @@ The onboarding `Column` takes the toolbar and navigation-bar paddings and consum |-----|------|-----| | Your chat | Each loaded chat of `rows.yourChats`, drawn with the chat list's own row, `ChatListNavLinkView(chat, remember { mutableStateOf(false) })` | as a local chat: opens it | | Result | [`DirectoryResultNavLink`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L297-L318)`(result, imageSize = 72.dp * fontSizeSqrtMultiplier, close = null, cleanup = { withApi { refreshStates() } })`: `ChatListNavLinkLayout` with [`DirectorySearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L204-L254) as its preview, so the row gets the chat rows' padding and divider and the chat rows' avatar size. The row: avatar, `#` + name, the directory's SimpleX name claim as plain text (truncates first), description (2 lines), then the caption (`body2`, secondary colour): the member or subscriber count, or "Already joining" for `Joining`; trailing "Open" (`open_verb`, primary colour, one line, never wrapped), except for `Joining` | `hideKeyboard(view)`, then [`openDirectoryResult`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L321-L324) (see Opening a result) | -| Load more | [`DirectoryLoadMoreRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L258-L276): `ic_zoom_in` (the magnifier with a plus, as iOS's `plus.magnifyingglass`) and "Load more · %1$d left" (`directory_search_load_more_n_left`) when `moreCount` is above 0, else "Load more"; not clickable and in the secondary colour while `loading` | `withApi { loadMore() }` | -| Retry | [`DirectoryRetryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L280-L292): `ic_refresh` and "Search failed, tap to retry" | `withApi { retry() }` | +| Load more | [`DirectoryLoadMoreRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L258-L276): `ic_zoom_in` (the magnifier with a plus, as iOS's `plus.magnifyingglass`) and "Load more · %1$d left" (`directory_search_load_more_n_left`) when `moreCount` is above 0, else "Load more"; not clickable and in the secondary colour while `loading` | `confirmDirectorySearch { withApi { loadMore() } }` | +| Retry | [`DirectoryRetryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L280-L292): `ic_refresh` and "Search failed, tap to retry" | `confirmDirectorySearch { withApi { retry() } }` | A result's menu is `ChatListNavLinkLayout`'s dropdown, opened by a long press, or by a right click on desktop, with one item, `ItemAction(open_verb, ic_chat_bubble)`, which does what a tap does. It has no lifted preview, unlike the iOS context menu, so `DirectorySearchRow` has no `showsOpen` parameter. Text from the directory is rendered with `Text(String)` (never parsed as markdown) with line limits.