From 33bae2658a77828d2d9bb2cfd0da1e21ddab842a Mon Sep 17 00:00:00 2001 From: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:53:11 +0000 Subject: [PATCH] android, desktop: document online search in spec and product docs apps/multiplatform/CODE.md requires spec and product updates with every code change. Mirror the iOS online search docs in the Kotlin docs' own formats and numbering, stating where Android and desktop differ, and fix the ConnectProgressManager signatures the branch's owner parameter had made stale. Run: 003-kotlin-directory-search, slice 6 --- apps/multiplatform/CODE.md | 3 + apps/multiplatform/product/concepts.md | 1 + .../multiplatform/product/flows/connection.md | 9 +- apps/multiplatform/product/gaps.md | 80 +++++++++++ apps/multiplatform/product/glossary.md | 6 +- apps/multiplatform/product/rules.md | 72 ++++++++++ apps/multiplatform/product/views/chat-list.md | 27 +++- apps/multiplatform/product/views/new-chat.md | 18 ++- apps/multiplatform/spec/README.md | 4 +- apps/multiplatform/spec/api.md | 26 +++- apps/multiplatform/spec/client/chat-list.md | 124 +++++++++++++++++- apps/multiplatform/spec/client/navigation.md | 29 ++++ apps/multiplatform/spec/impact.md | 61 +++++---- apps/multiplatform/spec/state.md | 8 ++ 14 files changed, 426 insertions(+), 42 deletions(-) diff --git a/apps/multiplatform/CODE.md b/apps/multiplatform/CODE.md index 30462d18e8..1be8a1a630 100644 --- a/apps/multiplatform/CODE.md +++ b/apps/multiplatform/CODE.md @@ -224,6 +224,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file) | common/.../common/AppLock.kt | spec/architecture.md | product/views/settings.md | | common/.../common/model/ChatModel.kt | spec/state.md | product/concepts.md | | common/.../common/model/SimpleXAPI.kt | spec/api.md, spec/architecture.md | product/concepts.md | +| common/.../common/model/DirectorySearch.kt | spec/client/chat-list.md, spec/api.md | product/views/chat-list.md, product/rules.md | | common/.../common/model/CryptoFile.kt | spec/services/files.md | product/flows/file-transfer.md | | common/.../common/platform/Core.kt | spec/architecture.md | product/concepts.md | | common/.../common/platform/AppCommon.kt | spec/architecture.md | product/flows/onboarding.md | @@ -245,6 +246,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file) | common/.../common/views/chatlist/ChatPreviewView.kt | spec/client/chat-list.md | product/views/chat-list.md | | common/.../common/views/chatlist/UserPicker.kt | spec/client/chat-list.md | product/views/chat-list.md | | common/.../common/views/chatlist/TagListView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/DirectorySearchView.kt | spec/client/chat-list.md | product/views/chat-list.md, product/views/new-chat.md | | common/.../common/views/chat/ChatView.kt | spec/client/chat-view.md | product/views/chat.md | | common/.../common/views/chat/ComposeView.kt | spec/client/compose.md | product/views/chat.md | | common/.../common/views/chat/SendMsgView.kt | spec/client/compose.md | product/views/chat.md | @@ -256,6 +258,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file) | common/.../common/views/call/WebRTC.kt | spec/services/calls.md | product/flows/calling.md | | common/.../common/views/newchat/NewChatView.kt | spec/client/navigation.md | product/views/new-chat.md | | common/.../common/views/newchat/AddGroupView.kt | spec/client/navigation.md | product/views/new-chat.md | +| common/.../common/views/newchat/NewChatSheet.kt | spec/client/navigation.md | product/views/new-chat.md | | common/.../common/views/usersettings/SettingsView.kt | spec/client/navigation.md | product/views/settings.md | | common/.../common/views/usersettings/Appearance.kt | spec/services/theme.md | product/views/settings.md | | common/.../common/views/usersettings/PrivacySettings.kt | spec/client/navigation.md | product/views/settings.md | diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md index d29ac5c2e5..be6048554c 100644 --- a/apps/multiplatform/product/concepts.md +++ b/apps/multiplatform/product/concepts.md @@ -50,6 +50,7 @@ This document provides a structured mapping between product-level concepts, thei | PC29 | User Address | [README.md](README.md) (Contacts, User Management) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/UserAddressView.kt`, `UserAddressLearnMore.kt` | `Controller.hs` (`APICreateMyAddress`) | | PC30 | Member Support Chat | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/MemberSupportView.kt`, `MemberSupportChatView.kt`, `MemberAdmission.kt` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | | PC31 | Channels (Relays) | [views/group-info.md](views/group-info.md) | [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/state.md](../spec/state.md) | `common/.../model/ChatModel.kt` (`RelayStatus` incl. `RsRejected`, `GroupRelay`, `GroupMemberRole.Relay`, `GroupMemberStatus.MemRejected`), `common/.../views/chat/group/ChannelRelaysView.kt`, `GroupMemberInfoView.kt` (rejected-status row), `common/.../views/newchat/AddChannelView.kt` (`RelayStatusIndicator` rejected branch), `common/.../views/chat/group/AddGroupRelayView.kt` | `Controller.hs` (`APIAddGroupRelays`, `APIAllowRelayGroup`, `XGrpRelayReject` CONF handler) | +| PC32 | Online Search (Directory and Names) | [views/chat-list.md](views/chat-list.md), [views/new-chat.md](views/new-chat.md), [rules.md](rules.md) (RULE-19 to RULE-23) | [spec/client/chat-list.md](../spec/client/chat-list.md) (section 11), [spec/client/navigation.md](../spec/client/navigation.md) (section 10), [spec/api.md](../spec/api.md) (sections 2.12, 7) | `common/.../views/chatlist/DirectorySearchView.kt` (`OnlineSearchModel`, `NameSearchRow`, `DirectorySearchRow`, `confirmDirectorySearch`), `common/.../model/DirectorySearch.kt` (`DirectorySearchEntry`, `simplexLinkType`, `planLocalChatId`, `NameSearchResult`, `OnlineSearchRows`), `common/.../views/chatlist/ChatListView.kt` (`ChatListSearchBar` triggers, `TagsOrConnectByName`, `nameRows`, `directoryRows`, onboarding branch), `common/.../views/newchat/NewChatSheet.kt` (`NewChatSheetLayout` online search), `common/.../views/helpers/SearchTextField.kt` (`onSearch`), `common/.../model/SimpleXAPI.kt` (`apiSearchDirectory`, `apiConnectPlan` `onFailure`, `directorySearchAlertShown`), `common/.../model/ChatModel.kt` (`ConnectProgressOwner`) | `Controller.hs` (`APISendServiceRequest`, `APIConnectPlan`); directory service `../../apps/simplex-directory-service/src/Directory/Rpc.hs` | **Legend for abbreviated paths:** - `common/.../` expands to `common/src/commonMain/kotlin/chat/simplex/common/` diff --git a/apps/multiplatform/product/flows/connection.md b/apps/multiplatform/product/flows/connection.md index 1b1123b535..36f961d836 100644 --- a/apps/multiplatform/product/flows/connection.md +++ b/apps/multiplatform/product/flows/connection.md @@ -197,9 +197,11 @@ suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? ### 6.1 ConnectProgressManager ```kotlin +enum class ConnectProgressOwner { Connect, DirectorySearch } + object ConnectProgressManager { - fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) - fun stopConnectProgress() + fun startConnectProgress(text: String, owner: ConnectProgressOwner = ConnectProgressOwner.Connect, onCancel: (() -> Unit)? = null) + fun stopConnectProgress(owner: ConnectProgressOwner = ConnectProgressOwner.Connect) fun cancelConnectProgress() } ``` @@ -208,6 +210,7 @@ object ConnectProgressManager { 2. After a 1-second delay, a progress indicator appears if the operation is still in progress. 3. On completion (success or failure), `stopConnectProgress` is called. 4. The user can cancel via `cancelConnectProgress`. +5. The online search shares the spinner with owner `DirectorySearch`: its `startConnectProgress` does nothing while a connection owns the spinner, and a `stopConnectProgress` from an owner that does not hold the spinner is ignored. `planAndConnect` calls `cancelConnectProgress()` first, so tapping a search result cancels the search before the connection takes the spinner. ### 6.2 Pending Connection States @@ -228,6 +231,6 @@ While connecting, the chat list shows a `PendingContactConnection` with status: | `InvitationLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, Connecting, Known | | `ContactAddressPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | | `GroupLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | -| `ConnectProgressManager` | `model/ChatModel.kt` | Manages connect progress indicator with timeout | +| `ConnectProgressManager` | `model/ChatModel.kt` | Manages connect progress indicator with timeout, owned by a connection or the online search (`ConnectProgressOwner`) | | `Contact` | `model/ChatModel.kt` | Established contact with profile, connection status | | `ContactRequest` | `model/ChatModel.kt` | Pending inbound contact request | diff --git a/apps/multiplatform/product/gaps.md b/apps/multiplatform/product/gaps.md index aae24bdca8..871e3f31ca 100644 --- a/apps/multiplatform/product/gaps.md +++ b/apps/multiplatform/product/gaps.md @@ -13,6 +13,9 @@ This document catalogs known gaps in the multiplatform codebase (Android and Des 5. [Documentation: Haskell Store Layer Not Fully Specified](#gap-05-documentation-haskell-store-layer-not-fully-specified) 6. [Desktop: Recording Not Implemented](#gap-06-desktop-recording-not-implemented) 7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented) +8. [Online Search: Groups Joined Through Another Link Still Appear as Directory Results](#gap-08-online-search-groups-joined-through-another-link-still-appear-as-directory-results) +9. [Online Search: First-Search Alert Does Not Mention the Server Hosting a Resolved Address](#gap-09-online-search-first-search-alert-does-not-mention-the-server-hosting-a-resolved-address) +10. [Online Search: New Chat Does Not Narrow Contacts by Name While Typing](#gap-10-online-search-new-chat-does-not-narrow-contacts-by-name-while-typing) --- @@ -289,3 +292,80 @@ This directly undermines RULE-02 (Database Encryption at Rest) and RULE-04 (Self - GAP-03 (Database Passphrase Not Enforced) is compounded by this gap on Desktop. - The `testCrypto()` function referenced in `AppCommon.desktop.kt:39` is commented out with a `// LALAL` marker, suggesting crypto testing was planned but never completed. + +--- + +## GAP-08: Online Search: Groups Joined Through Another Link Still Appear as Directory Results + +**Severity:** Low +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +A directory entry is matched to a local chat by a store-only lookup of its link (`apiConnectPlan` with `PlanResolveMode.PRMNever`, then `planLocalChatId`). A regular group's entry carries the directory's own group link (short link only), so a group the user joined through a different link, through the directory's full link, or by invitation is not found and is shown again in the Directory part. Tapping it goes through `planAndConnect`, which handles the known group. + +### Affected Locations + +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `OnlineSearchModel.request`, `OnlineSearchModel.rows` +- `common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt` -- `planLocalChatId` +- See [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +### Impact + +A group the user is already in can appear as a directory result with "Join group". Joining is not repeated: the connection plan opens the known group. + +### Recommendation + +Have the directory send a stable group identifier the core can match locally, or let the core match a directory short link against groups joined through the corresponding full link. + +--- + +## GAP-09: Online Search: First-Search Alert Does Not Mention the Server Hosting a Resolved Address + +**Severity:** Low +**Category:** Privacy / UX +**Platforms:** Android, Desktop + +### Description + +Resolving a SimpleX name with `PlanResolveMode.PRMUnknown` also loads the resolved address's link data from the SMP server that hosts it. The search text itself is not sent there, and the first-search alert ("Search in Directory?") names only SimpleX Directory and the name-resolving server. + +### Affected Locations + +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `showDirectorySearchAlert`, `OnlineSearchModel.resolveNames` +- `common/src/commonMain/moko-resources/base/strings.xml` -- `search_in_directory_alert_desc` +- See [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +### Impact + +The alert under-describes which servers learn of a name search: the server hosting the resolved address sees a link data request. + +### Recommendation + +Decide whether the alert copy should also say that the address a name points to is loaded. + +--- + +## GAP-10: Online Search: New Chat Does Not Narrow Contacts by Name While Typing + +**Severity:** Low +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +The chat list resolves a typed SimpleX name locally on each keystroke (debounced, `PlanResolveMode.PRMNever`) to narrow the list to the matching chat; the New chat sheet does not (its `ContactsSearchBar` resolves a name only when the "Connect to …" row is tapped). A known contact whose display name does not contain the typed name is therefore not in "Your contacts" and appears as a Names result instead; tapping it opens the existing chat. + +### Affected Locations + +- `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt` -- `ContactsSearchBar` typing collector, `NewChatSheetLayout` online search +- See [spec/client/navigation.md](../spec/client/navigation.md#10-new-chat-online-search) + +### Impact + +A contact the user already has can show up as an online Names result rather than in the contacts list. + +### Recommendation + +Reuse the chat list's local name resolution in `ContactsSearchBar` if this proves confusing. diff --git a/apps/multiplatform/product/glossary.md b/apps/multiplatform/product/glossary.md index 15ed40d3a1..c6ed43dac5 100644 --- a/apps/multiplatform/product/glossary.md +++ b/apps/multiplatform/product/glossary.md @@ -156,7 +156,7 @@ The sealed class representing all commands the app can send to the Haskell core **Groups:** `ApiNewGroup`, `ApiAddMember`, `ApiJoinGroup`, `ApiAcceptMember`, `ApiMembersRole`, `ApiBlockMembersForAll`, `ApiRemoveMembers`, `ApiLeaveGroup`, `ApiListMembers`, `ApiUpdateGroupProfile`, `APICreateGroupLink`, `APIDeleteGroupLink`, `APIGetGroupLink`, `ApiAddGroupShortLink` -**Connections:** `APIAddContact`, `APIConnect`, `APIConnectPlan`, `APIPrepareContact`, `APIPrepareGroup`, `APIConnectPreparedContact`, `APIConnectPreparedGroup`, `ApiConnectContactViaAddress` +**Connections:** `APIAddContact`, `APIConnect`, `APIConnectPlan`, `APIPrepareContact`, `APIPrepareGroup`, `APIConnectPreparedContact`, `APIConnectPreparedGroup`, `ApiConnectContactViaAddress`, `APISendServiceRequest` **Contacts:** `ApiDeleteChat`, `ApiClearChat`, `ApiListContacts`, `ApiUpdateProfile`, `ApiSetContactPrefs`, `ApiSetContactAlias` @@ -455,9 +455,9 @@ Singleton for displaying alert dialogs. Provides `showAlertMsg`, `showAlertDialo Holds the chat list state for a particular scope (main or secondary). Manages `chats` (State>), provides `updateChats()` to refresh, and supports filtering/keeping specific chats during updates. ### ConnectProgressManager -Tracks and displays connection progress in the UI. Methods: `startConnectProgress(text, onCancel)`, `stopConnectProgress()`, `cancelConnectProgress()`. Exposes `showConnectProgress` (nullable string indicating active progress text). +Tracks and displays connection progress in the UI. The single spinner is shared by connections and the online search, so it records its owner (`ConnectProgressOwner.Connect` or `ConnectProgressOwner.DirectorySearch`). Methods: `startConnectProgress(text, owner = Connect, onCancel)` (a search does not take the spinner from a running connection), `stopConnectProgress(owner = Connect)` (ignored when another owner holds the spinner), `cancelConnectProgress()` (cancels whoever owns it, running its `onCancel`). Exposes `showConnectProgress` (nullable string indicating active progress text, after about a second). -*See:* `ChatModel.kt:48` -- `object ConnectProgressManager` +*See:* `ChatModel.kt:52` -- `object ConnectProgressManager`; `ChatModel.kt:50` -- `enum class ConnectProgressOwner` ### withBGApi / withLongRunningApi Utility functions for launching coroutines on background threads. Used throughout the codebase to perform API calls without blocking the UI thread. diff --git a/apps/multiplatform/product/rules.md b/apps/multiplatform/product/rules.md index 90a2dadada..1b69e96a3f 100644 --- a/apps/multiplatform/product/rules.md +++ b/apps/multiplatform/product/rules.md @@ -12,6 +12,7 @@ This document specifies invariants enforced by the Android and Desktop (Kotlin/C 4. [File Transfer (RULE-14 through RULE-15)](#4-file-transfer) 5. [Notification Delivery (RULE-16 through RULE-17)](#5-notification-delivery) 6. [Call Integrity (RULE-18)](#6-call-integrity) +7. [Online Search (RULE-19 through RULE-23)](#7-online-search) --- @@ -251,3 +252,74 @@ This document specifies invariants enforced by the Android and Desktop (Kotlin/C - `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt` - Android: `android/src/main/java/chat/simplex/app/CallService.kt`, `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` - Desktop: `common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt` + +--- + +## 7. Online Search + +### RULE-19: Online Results Are Never Saved + +**Invariant:** Name and 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 `resolveNames` and `request` 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). + +**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/newchat/NewChatSheet.kt` -- `NewChatSheetLayout` + +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +--- + +### RULE-20: Only Group and Channel Links Are Accepted from the Directory + +**Invariant:** A directory entry MUST be shown only when its link is exactly one SimpleX link of type group or channel; any other entry (no link, an invitation or contact link, text around the link) MUST be dropped while the rest of the page is kept. Such a link is never passed to the core, which would fetch an invitation link from its server even in a store-only plan. + +**Enforcement:** `DirectorySearchEntry.link` is a `@Transient` property whose initializer, run by the decoder, takes `groupLink.connShortLink ?: groupLink.connFullLink` and throws `SerializationException` unless `simplexLinkType(link)` is `SimplexLinkType.group` or `SimplexLinkType.channel`; being `@Transient`, it cannot be set from the JSON. `parseDirectorySearchResponse` decodes entries one at a time inside `runCatching`, so a rejected entry is dropped alone. + +**Location:** `common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt` -- `DirectorySearchEntry`, `simplexLinkType()`, `parseDirectorySearchResponse()` + +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +--- + +### RULE-21: Online Lookups Never Show Alerts + +**Invariant:** Resolving a SimpleX name online and looking up directory entries locally MUST NOT show any alert, including the network retry alert; a failure gives no row. + +**Enforcement:** Every lookup of the online search calls `apiConnectPlan(…, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)`: `Log` only logs the failure, and a false `inProgress` also keeps `sendCmdWithRetry` from showing its retry alert. + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `ChatController.apiConnectPlan()`, `ConnectPlanFailure` +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `OnlineSearchModel.resolveNames()`, `OnlineSearchModel.request()` + +**Spec:** [spec/api.md](../spec/api.md#7-directory-search-and-connection-plans) + +--- + +### RULE-22: Nothing Is Shown Twice + +**Invariant:** A name result whose chat is shown in the local list MUST be hidden. A directory result MUST be hidden when its link refers, in the local store, to a chat shown in the local list or found by name, or when its link is the link a name was resolved through. Only store-derived chat ids and links resolved by the core may hide a row. Limit: a regular group joined through another link (or the directory's full link) is not found by the lookup and still appears as a directory result (GAP-08). + +**Enforcement:** `OnlineSearchModel.rows(shownChatIds)` filters at render time with the ids of the chats each screen shows; `planLocalChatId` derives a result's local chat id from the connection plan, and `NameSearchResult.resolvedLink` is the link the core resolved. + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `OnlineSearchModel.rows()` +- `common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt` -- `planLocalChatId()`, `NameSearchResult` + +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +--- + +### RULE-23: Consent Before the First Online Search + +**Invariant:** The search text MUST NOT be sent online until the user has confirmed the first-search alert, which names both recipients (SimpleX Directory and, for a SimpleX name, the server that resolves names), unless the user chose not to be asked again. + +**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.directorySearchAlertShown` is set and otherwise shows the alert. The preference is in `AppPreferences.hintPreferences`, so "Reset all hints" asks again. + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `confirmDirectorySearch()`, `showDirectorySearchAlert()` +- `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `AppPreferences.directorySearchAlertShown` + +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md index a09994d1e3..9242964223 100644 --- a/apps/multiplatform/product/views/chat-list.md +++ b/apps/multiplatform/product/views/chat-list.md @@ -51,6 +51,24 @@ Behavior: - Filters chat list in real-time by contact/group name via `filteredChats()` - Detects pasted SimpleX links (`strHasSingleSimplexLink`) and triggers `planAndConnect()` connection dialogue - In one-hand UI mode, search bar appears below tag filters with IME spacer; in standard mode, above tag filters +- For non-empty text that is not a link, offers "Search in Directory"; the keyboard Search key (Enter on desktop) searches online (see below) + +### Online Search (Names and Directory) + +Searching beyond the user's own chats happens only when the user asks, and the results are never saved: they disappear when the search text changes or the user switches profile. On Android, rotating the screen also drops them while the search text stays. + +| Element | Behavior | +|---|---| +| "Search in Directory" row | Shown for non-empty text that is not a SimpleX link. On Android with the one-hand UI it takes the place of the tags above the search field; with the top bar the tags stay and the row follows them below the field. On desktop the tags always stay and the row is shown next to them, on the search field's side. Searches SimpleX Directory for public groups and channels | +| Keyboard Search key | On Android the keyboard's Search action, on desktop Enter. Searches the directory and, when the text is a SimpleX name (e.g. `alice` or `#news.testing`), resolves the name online. For a bare name both the contact (`@`) and the channel (`#`) form are tried | +| "Connect to …" row | Shown for a SimpleX name, as before (on Android with the top bar it takes the tags' place); hidden once the online search found anything for that name | +| First-search alert | Before the first online search: "Search in Directory?" — the text goes to SimpleX Directory, and a SimpleX name also to the user's server that resolves names; no contact is created and the profile is not sent. Search / Search and don't show again / Cancel ("Reset all hints" brings it back) | +| Names part | Below the local chats, headed "Names": avatar, profile name, the SimpleX name in parentheses, description. Tapping connects (or opens the chat if it is already known) | +| Directory part | Below the Names part, headed "Directory": avatar, `#` name, the directory's SimpleX name, description, member or subscriber count, and "Join channel" / "Join group". Tapping connects; "Show more" loads the next page; a failed request shows "Search failed, tap to retry" | + +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. + +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`) @@ -122,9 +140,12 @@ Each chat type provides specific dropdown menu items: | State | Display | |---|---| | Loading | "Loading chats..." centered text | -| No chats | "You have no chats" centered text | +| No chats | "You have no chats" centered text, unless an online search runs or shows results | | No filtered chats | "No chats in list [tag name]" or "No unread chats" with clickable filter reset | -| No search results | "No chats found" centered text | +| No search results | "No chats found" centered text, unless an online search is running or shows results | +| Online search running | Progress indicator in the toolbar (after about a second); tapping a result or clearing the search cancels it | +| Directory search failed | "Search failed, tap to retry" row at the end of the Directory part | +| Online search found nothing | No Names or Directory part; "No chats found" when no local chat matches either (in onboarding, a "No chats found" row) | ## Source Files @@ -136,3 +157,5 @@ Each chat type provides specific dropdown menu items: | `UserPicker.kt` | `views/chatlist/UserPicker.kt` | | `TagListView.kt` | `views/chatlist/TagListView.kt` | | `GetStakeBanner.kt` | `views/chatlist/GetStakeBanner.kt` | +| `DirectorySearchView.kt` | `views/chatlist/DirectorySearchView.kt` | +| `DirectorySearch.kt` | `model/DirectorySearch.kt` | diff --git a/apps/multiplatform/product/views/new-chat.md b/apps/multiplatform/product/views/new-chat.md index b664fda67f..df441a2c67 100644 --- a/apps/multiplatform/product/views/new-chat.md +++ b/apps/multiplatform/product/views/new-chat.md @@ -1,6 +1,6 @@ # New Chat / Connection -> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/client/chat-list.md](../../spec/client/chat-list.md) (online search) ## Purpose @@ -83,6 +83,22 @@ Group creation flow: | QR scanner | Platform camera scanner for reading QR codes | | Short link display | Compact link text with copy/share actions | +### Search (New Chat Sheet) + +The search field of the New chat sheet filters "Your contacts" and offers the same online search as the chat list (see [Chat List, Online Search](chat-list.md#online-search-names-and-directory)): + +| Element | Behavior | +|---|---| +| "Connect to …" row | Below the field for a SimpleX name; hidden once the online search found anything for that name | +| "Search in Directory" row | Below the field (after "Connect to …") for non-empty text that is not a SimpleX link; searches SimpleX Directory | +| Keyboard Search key | On Android the keyboard's Search action, on desktop Enter. Searches the directory and, for a SimpleX name, resolves the name online | +| Names and Directory sections | After the contacts, each with a section header in the style of "Your contacts". Tapping a result connects and closes the sheet, as other New chat actions do. A result for a contact already listed is not repeated. With the one-hand UI the list is reversed, so a header is shown below its rows, as "Your contacts" is | +| Contacts empty state | "No filtered contacts" is not shown while the online search runs or shows results | +| Online search running | Progress indicator in the search bar; closing the sheet cancels the search | +| Directory search failed | "Search failed, tap to retry" row at the end of the Directory section | + +The first online search asks for consent with the same alert as on the chat list. Results disappear when the text changes or the user switches profile (which also closes the sheet), and when a sheet opened from New chat covers it. Archived contacts keep their own search field without online results; their Search key does nothing. + ## Source Files | File | Path | diff --git a/apps/multiplatform/spec/README.md b/apps/multiplatform/spec/README.md index c6dbf1abf2..152913c5f5 100644 --- a/apps/multiplatform/spec/README.md +++ b/apps/multiplatform/spec/README.md @@ -89,9 +89,9 @@ Common Module (commonMain) | Database | [spec/database.md](database.md) | SQLite database files, migrations, encryption, backup/restore | | Impact | [spec/impact.md](impact.md) | Source file → product concept mapping for change impact analysis | | Chat View | [spec/client/chat-view.md](client/chat-view.md) | ChatView, ChatItemView, message rendering, item interactions | -| Chat List | [spec/client/chat-list.md](client/chat-list.md) | ChatListView, ChatPreviewView, filtering, search, tags | +| Chat List | [spec/client/chat-list.md](client/chat-list.md) | ChatListView, ChatPreviewView, filtering, search, tags, online search (SimpleX Directory and names) | | Compose | [spec/client/compose.md](client/compose.md) | ComposeView, SendMsgView, ComposeState, attachments, mentions | -| Navigation | [spec/client/navigation.md](client/navigation.md) | App screen routing, onboarding, settings, new chat flows | +| Navigation | [spec/client/navigation.md](client/navigation.md) | App screen routing, onboarding, settings, new chat flows, New chat online search | | Calls | [spec/services/calls.md](services/calls.md) | WebRTC call lifecycle, signaling, platform-specific call views | | Files | [spec/services/files.md](services/files.md) | File transfer (SMP inline / XFTP), CryptoFile encryption, platform file paths | | Notifications | [spec/services/notifications.md](services/notifications.md) | NtfManager, SimplexService, notification channels, background delivery | diff --git a/apps/multiplatform/spec/api.md b/apps/multiplatform/spec/api.md index 4114e9de4f..67fe6653d8 100644 --- a/apps/multiplatform/spec/api.md +++ b/apps/multiplatform/spec/api.md @@ -15,10 +15,12 @@ - 2.9 [Chat Tags](#29-chat-tags) - 2.10 [Server Operators](#210-server-operators) - 2.11 [Archive](#211-archive) + - 2.12 [Service Requests](#212-service-requests) 3. [Response Types](#3-response-types) 4. [Event Types](#4-event-types) 5. [Error Types](#5-error-types) 6. [Source Files](#6-source-files) +7. [Directory Search and Connection Plans](#7-directory-search-and-connection-plans) --- @@ -180,7 +182,7 @@ All functions below are `suspend fun` members of `ChatController` ([SimpleXAPI.k | `apiAddContact` | `rh: Long?, incognito: Boolean` | Create a one-time invitation link for a new contact | [L1444](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1444) | | `apiSetConnectionIncognito` | `rh: Long?, connId: Long, incognito: Boolean` | Toggle incognito on a pending connection | [L1455](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1455) | | `apiChangeConnectionUser` | `rh: Long?, connId: Long, userId: Long` | Change the user profile on a pending connection | [L1464](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1464) | -| `apiConnectPlan` | `rh: Long?, connLink: String, inProgress: MutableState` | Analyze a connection link before connecting | [L1474](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1474) | +| `apiConnectPlan` | `rh: Long?, connLink: String, resolveMode: PlanResolveMode = PRMUnknown, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState, onFailure: ConnectPlanFailure = Alert` | Analyze a connection link (or resolve a SimpleX name) before connecting; returns `ConnectionPlanResult?`. Failure handling: see [section 7](#7-directory-search-and-connection-plans) | [L1654](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1654-L1668) | | `apiConnect` | `rh: Long?, incognito: Boolean, connLink: CreatedConnLink` | Connect via an invitation or address link | [L1482](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1482) | | `apiPrepareContact` | `rh, connLink, contactShortLinkData` | Prepare a contact chat from a short link (before connecting) | [L1546](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1546) | | `apiPrepareGroup` | `rh, connLink, groupShortLinkData` | Prepare a group chat from a short link (before connecting) | [L1555](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1555) | @@ -288,6 +290,12 @@ class ArchiveConfig( ) ``` +### 2.12 Service Requests + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiSearchDirectory` | `rh: Long?, text: String, cursor: JsonObject?` | Sends [`CC.APISendServiceRequest`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4069) (`userId, target, timeoutSec: Double?, request: JsonObject`; `/_service_request`) to SimpleX Directory and waits up to the timeout for its response, `CRServiceResponse`; see [section 7](#7-directory-search-and-connection-plans) | [L1645](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1645-L1652) | + --- @@ -322,6 +330,7 @@ The `CR` sealed class ([SimpleXAPI.kt#L6114](../common/src/commonMain/kotlin/cha | Remote ctrl | `RemoteCtrlList`, `RemoteCtrlFound`, `RemoteCtrlConnected`, `RemoteCtrlStopped` | L6264-L6269 | | Encryption | `ContactPQAllowed`, `ContactPQEnabled` | L6271-L6272 | | Misc | `CmdOk`, `ArchiveExported`, `ArchiveImported`, `AppSettingsR`, `VersionInfo` | L6274-L6283 | +| Service | `CRServiceResponse` (`user, responseData: JsonObject`): a service's response to `APISendServiceRequest`, left undecoded for the caller | [L6791](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6791) | | Fallback | `Response` (unknown type + raw JSON), `Invalid` (unparseable) | L6282-L6283 | Each `CR` subclass is annotated with `@Serializable @SerialName("jsonTag")` for polymorphic JSON deserialization. @@ -432,5 +441,20 @@ sealed class ArchiveError { | Cryptor.kt | Platform-expect encryption interface | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | | Cryptor.android.kt | Android: AndroidKeyStore AES-GCM encryption | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | | Cryptor.desktop.kt | Desktop: placeholder (no-op) encryption | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | +| DirectorySearch.kt | Directory request and response, online search result types | `common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt` | + +--- + +## 7. Directory Search and Connection Plans + +`ChatController` functions in [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) used by the online search (see [Chat List, Online Search](client/chat-list.md#11-online-search)): + +| Function | Description | Line | +|----------|-------------|------| +| `apiSearchDirectory(rh, text, cursor)` | Sends `CC.APISendServiceRequest` to [`DIRECTORY_SERVICE_LINK`](../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L16) with [`DIRECTORY_SEARCH_TIMEOUT_SEC`](../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L19) and the request built by [`directorySearchRequest`](../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L70-L74); returns [`parseDirectorySearchResponse`](../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L78-L87)`(responseData)`, or null on any error (logged with `responseType` and `details`). Blocks for up to the timeout; uses plain `sendCmd`, so a timeout gives a retry row rather than the alert `sendCmdWithRetry` would show | [L1645](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1645-L1652) | +| `apiConnectPlan(rh, connLink, resolveMode, linkOwnerSig, inProgress, onFailure)` | Plans a connection; returns `ConnectionPlanResult?`. `NotResolvedLocally` (a `PRMNever` lookup that matched nothing) returns null silently; any other failure is handled by `onFailure` | [L1654](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1654-L1668) | +| `ConnectPlanFailure` | `Alert` (default, every existing caller): `apiConnectResponseAlert`, only while `inProgress.value` is true, as before; `Log`: a debug log only, used by the online search so name resolution and per-entry lookups never raise alerts | [L7522](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7522) | + +A service response is carried as a kotlinx `JsonObject` (any JSON), so the API layer does not depend on a service's own types; `parseDirectorySearchResponse` reads its fields and decodes each entry with `json.decodeFromJsonElement`. All paths are relative to `apps/multiplatform/`. diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md index e34da9613d..2c9e72d738 100644 --- a/apps/multiplatform/spec/client/chat-list.md +++ b/apps/multiplatform/spec/client/chat-list.md @@ -1,6 +1,6 @@ # Chat List Specification -Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt` +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt`, `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt`, `common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt` --- @@ -16,6 +16,7 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatLis 8. [UserPicker](#8-userpicker) 9. [Source Files](#9-source-files) 10. [Crowdfunding Banner](#10-crowdfunding-banner) +11. [Online Search](#11-online-search) --- @@ -152,6 +153,7 @@ The `filteredChats` function (line ~1474) applies filters in this order: - Text input with search icon. - SimpleX link detection: When a pasted string contains a single SimpleX link, it triggers `planAndConnect` for connection, suppressing normal search. - Unread filter toggle button (right side, when search is empty). +- Searching SimpleX Directory and resolving SimpleX names online happen only on request, below the local results: see [Online Search](#11-online-search). --- @@ -314,6 +316,8 @@ Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow` | `TagListView.kt` | Tag management and assignment view | | `UserPicker.kt` | User switching side panel | | `GetStakeBanner.kt` | Crowdfunding banner and the shared banner card chrome | +| `DirectorySearchView.kt` | Online search model, Names and Directory rows, first-search alert | +| `model/DirectorySearch.kt` | Directory response parsing and online result types | --- @@ -358,3 +362,121 @@ Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage ### Shared card chrome `Modifier.bannerCard(onTap)` (size state, gradient brush, `heightIn`, `clip`, `background`, `clickable`) and `BannerDismissButton` are declared separately from `GetStakeBanner` so other banners can adopt the same chrome; the paddings stay with the caller. The gradient reuses `gradientPoints`, `lightStops` and `darkStops` from `views/newchat/OnboardingCards.kt`. + +--- + + + +## 11. Online Search + +**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, and, when the text is a SimpleX name, to the user's name-resolving server. Results are shown below the local chats in two parts, Names then Directory. 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#L866-L970) 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#L911-L916)). 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 + +| Trigger | Handler | Runs | +|---------|---------|------| +| Search key: the keyboard's Search action on Android, Enter (key up) on desktop, through [`SearchTextField`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L33-L43)`(onSearch)` | [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L995-L1000) | `search(text, name = connectNameCandidate.value)`: the directory's first page, and name resolution when the text is a name candidate | +| "Search in Directory" row ([`SearchInDirectoryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L202-L217)) | [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L988-L993) | `searchDirectory(text)`: the directory only | + +`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. + +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#L985-L986), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L327-L333), 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#L335-L363), `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#L1298-L1352) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1219-L1244): + +| Search text | Android, one-hand UI (slot above the field) | Android, top bar (slot below the field) | Desktop (either mode) | +|-------------|-----------------------------------|------------------------------|------| +| Empty | Tags | Tags | Tags | +| Plain text | "Search in Directory" | Tags, then "Search in Directory" | Tags and "Search in Directory" | +| Name candidate, no name results yet | "Connect to …" and "Search in Directory" | "Connect to …" (in the tags' place), then "Search in Directory" | Tags, "Connect to …" and "Search in Directory" | +| Name candidate with name results | "Search in Directory" | Tags, then "Search in Directory" | Tags and "Search in Directory" | +| Pasted SimpleX link | Tags | Tags | Tags | + +On desktop the rows are passed to the caller's `desktopView`, which places them between the tags and the field: above the field with the bottom toolbar, below it with the top toolbar. + + + +### OnlineSearchModel + +**Location:** [`OnlineSearchModel`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L31-L199) + +A plain class holding Compose state, owned by `ChatListView` as `remember { OnlineSearchModel() }` and passed through `ChatListWithLoadingScreen` to `ChatList`, `ChatListSearchBarWithTags`, `ChatListSearchBar` and `TagsOrConnectByName`. Every method runs on the main thread: the triggers and rows launch them with `withApi` (`Dispatchers.Main`), and the spinner's `onCancel` hops back with `withApi { cancelRequest() }`; the API calls move the FFI work to `Dispatchers.IO` themselves. This keeps every read and write of `generation` on one thread. + +| State | Description | +|-------|-------------| +| `names: List` | Name results for the current text | +| `directory: List` | Directory entries shown so far, each with the local chat its link refers to | +| `loading` | A directory request is in flight | +| `resolvingNames` | Name resolution is in flight | +| `failed` | The last directory request failed | +| `searched` | A directory request for the current text completed, successfully or not (tells "not searched" from "found nothing") | +| `cursor` (private) | Opaque `JsonObject?` from the last page, echoed back for the next one; null when there are no more. Snapshot state, because `footer` reads it | +| `generation` (private) | Bumped by `reset()` and `cancelRequest()`; a reply whose captured generation differs never writes model state | + +The public state has private setters; `searchedText`, `generation` and `spinnerShown` are plain fields. + +| Computed | Definition | +|----------|------------| +| [`searching`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L56) | `loading \|\| resolvingNames` | +| [`showResults`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L57) | `searching \|\| searched \|\| names.isNotEmpty()` | +| [`footer`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L58) | `DirectoryFooter.Retry` when `failed`, else `ShowMore` when a cursor exists, else null | + +| Method | Description | +|--------|-------------| +| [`reset()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L60-L70) | Clears all results and flags, bumps `generation` (text change, profile switch) | +| [`cancelRequest()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L73-L77) | The spinner's `onCancel`: bumps `generation` and clears `loading` and `resolvingNames`; results already shown stay | +| [`search(text, name)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L79-L96) | Search key: ignored for empty text or while `searching` for the same text; otherwise `reset()`, then the first directory page and, with a name, name resolution concurrently (`coroutineScope { async { resolveNames(name, gen) } }`, with the generation captured before the child coroutine starts) | +| [`searchDirectory(text)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L98-L111) | The row: ignored while `loading` for the same text; for the same text it clears only the directory state (name results stay), for new text it resets; then the first page | +| [`loadMore()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L113-L116) | "Show more": the next page, appended | +| [`retry()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L119-L125) | The retry row: resumes the page when a cursor exists, else repeats the first page; no reset | +| [`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L127-L139) | The rows to render, given the ids of the local chats shown (see De-duplication) | + +**Spinner.** The private setters of `loading` and `resolvingNames` call [`updateSpinner()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L185-L198) on every write, which starts the "Searching directory…" progress with owner `ConnectProgressOwner.DirectorySearch` when `searching` becomes true and stops it when it becomes false (`spinnerShown` keeps the last state, so repeated writes do nothing). The chat list shows it in the toolbar ([`ChatListToolbar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L639-L645)) once it has run for a second, not in the search field. The spinner is shared with connections: [`ConnectProgressOwner`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L50) records whose it is, [`startConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L60-L69) does not let a search take it from a running connection, and [`stopConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L72-L78) ignores a stop from another owner. Tapping a result calls [`planAndConnect`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt#L24-L55), whose `cancelConnectProgress()` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L81-L88)) cancels the search through its `onCancel` before the connection takes the spinner. Clearing the search text does the same through the typing path. + +### Directory requests + +[`request(append)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L159-L183) reads `chatModel.remoteHostId()` and calls [`apiSearchDirectory`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1645-L1652), which sends `APISendServiceRequest` to [`DIRECTORY_SERVICE_LINK`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L16) with a [10-second timeout](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L19) and the request built by [`directorySearchRequest`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L70-L74) (`searchText`, and `searchCursor` for later pages). [`parseDirectorySearchResponse`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L78-L87) accepts only a `searchResults` response and decodes its entries one at a time inside `runCatching`, so an entry the app cannot decode is dropped and the rest of the page is kept. + +[`DirectorySearchEntry`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L33-L57) keeps the wire field `groupLink` private; its [`link`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L47-L56) is a `@Transient` property whose initializer, run by the generated decoder, takes `groupLink.connShortLink ?: groupLink.connFullLink` and throws `SerializationException` unless [`simplexLinkType`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L59-L62) classifies it as `SimplexLinkType.group` or `SimplexLinkType.channel`. Being `@Transient`, `link` cannot be set by a `link` key in the JSON. `simplexLinkType` returns the link type only when [`strConnectTarget`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt#L834) finds exactly one SimpleX link whose text is the whole string. `link` is also the entry's `id`. The directory sends only the short link when an entry has one. + +For each new entry of a page (not already shown, not repeated in the page), the request looks up the local chat its link refers to with `apiConnectPlan(rh, entry.link, resolveMode = PRMNever, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)` and [`planLocalChatId`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L89-L109), building a [`DirectoryResult`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L172-L177). The link-type check makes this lookup store-only: the core plans contact-mode links under `PRMNever` from the store, but would fetch an invitation link from its server. The page is published, appended or replacing, only if the generation did not change. + +### Name resolution + +[`resolveNames(name, gen)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L141-L157) 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#L1261-L1272), 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#L111-L170) (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 + +[`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L127-L139) returns an [`OnlineSearchRows`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L179-L185) at composition time, so pages added by "Show more" are filtered too: + +- a name result is hidden when its `localChatId` is among the shown local chats; +- a directory result is hidden when its `localChatId` is among the shown local chats or any name result's `localChatId`, or when its `entry.link` equals any name result's `resolvedLink` (all name results, shown or not); +- `footer` passes through. + +Only store-derived chat ids and the link the core resolved can hide a row, never a field the directory or a name's owner wrote. A mismatch in link encoding can only produce a duplicate row. + +[GAP] A regular group the user joined through a different link, or through the directory's full link, is not found by the store lookup and still appears as a Directory row; tapping it goes through `planAndConnect`. See [GAP-08](../../product/gaps.md#gap-08-online-search-groups-joined-through-another-link-still-appear-as-directory-results). + +### Rendering + +| Where | Rows | Empty state | +|-------|------|-------------| +| [`ChatList`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1045-L1217) | `rows(shownChatIds)` with the ids of the shown chats ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1088)); after the local chats and before `ChatListFeatureCards`, [`nameRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L509-L523) then [`directoryRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L525-L544) | `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#L1195-L1200)); 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-L498) | 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#L1214-L1216)) 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 starts with a header `item` ([`SearchPartHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L546-L554): `body2`, secondary colour, `semantics { heading() }`) rather than a `stickyHeader`, and its rows are `items` keyed by `id`. In the reversed one-hand list a header therefore renders below its rows. + +| Row | View | Tap | +|-----|------|-----| +| Name | [`NameSearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L266-L294): avatar, profile name in bold, the SimpleX name in parentheses (secondary, one `AnnotatedString`, one line), description (2 lines) | `planAndConnect(rhId, result.target, close = null, cleanup = null)` | +| Directory entry | [`DirectorySearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L219-L264): avatar, `#` + name, the directory's SimpleX name claim as plain text (truncates first), description (2 lines), member or subscriber count, and the verb "Join channel" / "Join group" (one line, never wrapped) | `planAndConnect(rhId, result.entry.link, close = null, cleanup = null)` | +| Retry | [`DirectoryRetryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L313-L325): "Search failed, tap to retry" | `withApi { retry() }` | +| Show more | [`DirectoryShowMoreRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L296-L311): "Show more", disabled (secondary colour) while `loading` | `withApi { loadMore() }` | + +A name or directory row tap first hides the keyboard (`hideKeyboard(view)`), then runs `planAndConnect` with `withBGApi`. Text from the directory and from name owners is rendered with `Text(String)` (never parsed as markdown) with line limits. + +[GAP] Resolving a SimpleX name with `PRMUnknown` also loads the resolved address's link data from the server that hosts it; the alert names only the directory and the name-resolving server. See [GAP-09](../../product/gaps.md#gap-09-online-search-first-search-alert-does-not-mention-the-server-hosting-a-resolved-address). diff --git a/apps/multiplatform/spec/client/navigation.md b/apps/multiplatform/spec/client/navigation.md index 96dd58052c..652072efa7 100644 --- a/apps/multiplatform/spec/client/navigation.md +++ b/apps/multiplatform/spec/client/navigation.md @@ -15,6 +15,7 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (470 lines) 7. [Authentication Gate](#7-authentication-gate) 8. [Onboarding Flow](#8-onboarding-flow) 9. [Source Files](#9-source-files) +10. [New Chat Online Search](#10-new-chat-online-search) --- @@ -376,3 +377,31 @@ The stage value is stored in `appPrefs.onboardingStage` and persisted across app | `views/SplashView.kt` | Splash / loading screen | | `views/call/CallView.kt` | In-call fullscreen view (ActiveCallView) | | `views/localauth/PasswordEntry.kt` | Column divider utility (contains VerticalDivider) | +| `views/newchat/NewChatSheet.kt` | New chat sheet: action buttons, contacts list, online search sections; Archived contacts (`DeletedContactsView`) | + +--- + + + +## 10. New Chat Online Search + +**Location:** [`NewChatSheetLayout`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L110-L535) + +`NewChatSheetLayout` offers the chat list's online search (see [Chat List, Online Search](chat-list.md#11-online-search)) with its own [`OnlineSearchModel`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L141) (`remember`), reset from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L182-L187)). A profile switch also closes all modals on Kotlin, so the sheet is disposed anyway. The sheet's `DisposableEffect` `onDispose` ([`NewChatSheet`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L42-L48)) calls `cancelConnectProgress()`, which cancels a search still in flight. The model is also dropped while a modal opened from the sheet covers it (`ModalManager` composes only the top modal) and on an Android rotation. + +| Element | Where | Behavior | +|---------|-------|----------| +| Search key | [`ContactsSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L555-L657) with `onSearch` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L563)), passed to `SearchTextField` | the keyboard's Search action (Android) or Enter (desktop) runs [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L237-L242): `search(text, name = connectNameCandidate.value)` | +| "Connect to …" row | the sticky header, below the field, in both `OneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L387-L391)) and `NonOneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L485-L489)) | shown for a name candidate until name results arrive (`candidate != null && onlineSearch.names.isEmpty()`) | +| "Search in Directory" row | after the "Connect to …" row, behind a `Divider` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L392-L395), [source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L490-L493)) | shown for non-empty, non-link text; runs [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L231-L235): `searchDirectory(text)` | +| Names and Directory sections | after `itemsIndexed(filteredContactChats)` in both columns, [`onlineSearchSections(rows)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L195-L227) | each part starts with an [`OnlineSearchHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L189-L193) item styled as "Your contacts" (`SectionDividerSpaced` then the `SectionView` title, not upper-cased); rows are `items` keyed by `id`; the Directory part ends with the retry or "Show more" row; rows tap `hideKeyboard(view)` then `withBGApi { planAndConnect(rhId, …, close = close, cleanup = null) }`, which closes the sheet once connected | + +Both triggers do nothing for empty text or a pasted link ([`searchTrimmed()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L229), `searchShowingSimplexLink`) and go through `confirmDirectorySearch`, launching the model with `withApi`. `onlineSearchSections`, `OnlineSearchHeader`, `searchTrimmed`, `runDirectorySearch` and `runOnlineSearch` are local functions of `NewChatSheetLayout`. In the reversed one-hand column a header item renders below its rows, as the contacts header does. + +**De-duplication against the contacts list.** `filteredContactChats` (the contacts the list shows, computed in the layout's scope) gives the shown ids: `rows = onlineSearch.rows(shownChatIds = filteredContactChats.map { it.id }.toSet())` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L180)), so no extra helper is needed. + +**Empty state.** `showsEmptyState = !onlineSearch.searching && rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L181)); [`NoFilteredContactsItem`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L324-L338) shows "No filtered contacts" only when it holds. Kotlin has no "No contacts" state. + +**Archived contacts.** [`DeletedContactsView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L750-L864) passes no `onSearch` to `ContactsSearchBar`: its field keeps the Search IME label (every `SearchTextField` uses `ImeAction.Search`), the key does nothing, and it shows no online rows; its empty state is unchanged. + +[GAP] New chat does not resolve a typed name locally while typing (the `ContactsSearchBar` collector resolves a name only when its "Connect to …" row is tapped), so a known contact whose name does not contain the typed text is not in "Your contacts" and appears as a Names row; tapping it opens the existing chat. See [GAP-10](../../product/gaps.md#gap-10-online-search-new-chat-does-not-narrow-contacts-by-name-while-typing). diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md index 7e0fb181d8..4c7045f961 100644 --- a/apps/multiplatform/spec/impact.md +++ b/apps/multiplatform/spec/impact.md @@ -41,6 +41,7 @@ | PC29 | User Address | | PC30 | Member Support Chat | | PC31 | Channels (Relays) | +| PC32 | Online Search (Directory and Names) | --- @@ -52,13 +53,14 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `App.kt` | PC1 through PC31 | High | Root composable — navigation scaffold for all features | +| `App.kt` | PC1 through PC32 | High | Root composable — navigation scaffold for all features | | `AppLock.kt` | PC22 | Medium | App lock state and authorization lifecycle | -| `model/ChatModel.kt` | PC1 through PC31 | High | Central state object — every feature reads or writes here | -| `model/SimpleXAPI.kt` | PC1 through PC31 | High | FFI bridge to Haskell core — all commands and responses | +| `model/ChatModel.kt` | PC1 through PC32 | High | Central state object — every feature reads or writes here | +| `model/SimpleXAPI.kt` | PC1 through PC32 | High | FFI bridge to Haskell core — all commands and responses | +| `model/DirectorySearch.kt` | PC32 | High | Directory response parsing and link-type validation of untrusted entries; online result types | | `model/CryptoFile.kt` | PC10, PC23 | Medium | Encrypted file read/write helpers | -| `platform/Core.kt` | PC1 through PC31 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | -| `platform/AppCommon.kt` | PC1 through PC31 | Medium | Shared app initialization logic | +| `platform/Core.kt` | PC1 through PC32 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | +| `platform/AppCommon.kt` | PC1 through PC32 | Medium | Shared app initialization logic | | `platform/Files.kt` | PC10, PC23, PC26 | Medium | File path resolution, temp dirs, encryption utilities | | `platform/NtfManager.kt` | PC18 | High | Notification manager expect declarations | | `platform/Notifications.kt` | PC18 | Medium | Notification channel and permission abstractions | @@ -68,7 +70,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | `platform/Cryptor.kt` | PC23 | Medium | Keystore encryption expect declarations | | `platform/Share.kt` | PC10, PC12 | Low | Share sheet abstractions | | `platform/Images.kt` | PC10, PC19 | Low | Image processing utilities | -| `platform/Platform.kt` | PC1 through PC31 | Low | Platform detection and capability flags | +| `platform/Platform.kt` | PC1 through PC32 | Low | Platform detection and capability flags | | `platform/PlatformTextField.kt` | PC4 | Low | Native text input expect declarations | | `platform/Back.kt` | PC1 | Low | Back navigation handling | | `platform/UI.kt` | PC24 | Low | UI density and locale helpers | @@ -91,8 +93,9 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `views/chatlist/ChatListView.kt` | PC1, PC28 | High | Main screen — chat list rendering and search | +| `views/chatlist/ChatListView.kt` | PC1, PC28, PC32 | High | Main screen — chat list rendering and search; online search triggers, result parts and the onboarding search | | `views/chatlist/GetStakeBanner.kt` | PC1 | Low | Crowdfunding banner and shared banner card chrome | +| `views/chatlist/DirectorySearchView.kt` | PC1, PC32 | Medium | Online search model (generation, spinner, de-duplication), result rows, consent alert | | `views/chatlist/ChatListNavLinkView.kt` | PC1, PC2, PC3 | Medium | Navigation from chat list item to chat | | `views/chatlist/ChatPreviewView.kt` | PC1, PC2, PC3, PC11 | Medium | Chat row preview rendering | | `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI | @@ -190,7 +193,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| | `views/newchat/NewChatView.kt` | PC12, PC29 | High | New connection creation — onramp for all contacts | -| `views/newchat/NewChatSheet.kt` | PC12 | Medium | Bottom sheet with connection options | +| `views/newchat/NewChatSheet.kt` | PC12, PC32 | Medium | Bottom sheet with connection options, contacts list and online search sections; search bar shared with archived contacts | | `views/newchat/ConnectPlan.kt` | PC12, PC15 | Medium | Link parsing and connection plan resolution | | `views/newchat/AddGroupView.kt` | PC3, PC14 | Medium | New group creation flow | | `views/newchat/AddChannelView.kt` | PC31 | Medium | Public channel creation, channel link card, `RelayStatusIndicator` | @@ -270,9 +273,9 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `views/helpers/AlertManager.kt` | PC1 through PC31 | Medium | Modal alert system used across all features | -| `views/helpers/ModalView.kt` | PC1 through PC31 | Medium | Modal navigation stack | -| `views/helpers/Utils.kt` | PC1 through PC31 | Low | Shared formatting, clipboard, and utility functions | +| `views/helpers/AlertManager.kt` | PC1 through PC32 | Medium | Modal alert system used across all features | +| `views/helpers/ModalView.kt` | PC1 through PC32 | Medium | Modal navigation stack | +| `views/helpers/Utils.kt` | PC1 through PC32 | Low | Shared formatting, clipboard, and utility functions | | `views/helpers/DatabaseUtils.kt` | PC23 | Medium | Keystore passphrase and database helpers | | `views/helpers/LinkPreviews.kt` | PC11 | Medium | Link preview fetching and rendering | | `views/helpers/LocalAuthentication.kt` | PC22 | Medium | Biometric/passcode authentication expect | @@ -282,7 +285,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | `views/helpers/ChooseAttachmentView.kt` | PC10 | Low | Attachment picker | | `views/helpers/GetImageView.kt` | PC10, PC19 | Low | Image capture and crop | | `views/helpers/TextEditor.kt` | PC4 | Low | Rich text editor helpers | -| `views/helpers/SearchTextField.kt` | PC1 | Low | Search bar composable | +| `views/helpers/SearchTextField.kt` | PC1, PC32 | Low | Search bar composable; optional Search key handler (`onSearch`: Android IME action, desktop Enter) | | `views/helpers/CustomTimePicker.kt` | PC8 | Low | Time picker for timed messages | | `views/helpers/DragAndDrop.kt` | PC10 | Low | Drag-and-drop file handling | | `views/helpers/ProcessedErrors.kt` | — | Low | Error aggregation | @@ -325,8 +328,8 @@ Path prefix: `android/src/main/java/chat/simplex/app/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `SimplexApp.kt` | PC1 through PC31 | High | Application class — initializes core, preferences, and notification channels | -| `MainActivity.kt` | PC1 through PC31 | High | Single-activity host — intent handling, lifecycle, deep links | +| `SimplexApp.kt` | PC1 through PC32 | High | Application class — initializes core, preferences, and notification channels | +| `MainActivity.kt` | PC1 through PC32 | High | Single-activity host — intent handling, lifecycle, deep links | | `SimplexService.kt` | PC18 | High | Foreground service — keeps message receiver alive | | `CallService.kt` | PC17 | Medium | Foreground service for active calls | | `MessagesFetcherWorker.kt` | PC18 | Medium | WorkManager periodic message fetch | @@ -340,7 +343,7 @@ Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `platform/AppCommon.android.kt` | PC1 through PC31 | Medium | Android app initialization actual declarations | +| `platform/AppCommon.android.kt` | PC1 through PC32 | Medium | Android app initialization actual declarations | | `platform/SimplexService.android.kt` | PC18 | Medium | Android foreground service actual implementation | | `platform/Files.android.kt` | PC10, PC23, PC26 | Medium | Android file paths and content-URI resolution | | `platform/Cryptor.android.kt` | PC23 | Medium | Android Keystore encryption actual implementation | @@ -406,7 +409,7 @@ Path prefix: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/` | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `Main.kt` | PC1 through PC31 | High | JVM entry point — Haskell init, migrations, app launch | +| `Main.kt` | PC1 through PC32 | High | JVM entry point — Haskell init, migrations, app launch | ### 3.2 Desktop Platform Implementations (desktopMain) @@ -417,7 +420,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/` | `DesktopApp.kt` | PC1, PC2, PC3 | High | Desktop Compose window — window lifecycle, crash recovery | | `StoreWindowState.kt` | — | Low | Window position/size persistence | | `model/NtfManager.desktop.kt` | PC18 | Medium | Desktop system tray notification display | -| `platform/AppCommon.desktop.kt` | PC1 through PC31 | Medium | Desktop app initialization actual declarations | +| `platform/AppCommon.desktop.kt` | PC1 through PC32 | Medium | Desktop app initialization actual declarations | | `platform/SimplexService.desktop.kt` | PC18 | Low | Desktop background receiver (no foreground service) | | `platform/Files.desktop.kt` | PC10, PC23, PC26 | Medium | Desktop file path resolution | | `platform/Cryptor.desktop.kt` | PC23 | Medium | Desktop keystore encryption actual implementation | @@ -480,13 +483,13 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| `src/Simplex/Chat.hs` | PC1 through PC31 | High | Main chat module — top-level orchestration | -| `src/Simplex/Chat/Controller.hs` | PC1 through PC31 | High | Command processor — all API commands dispatched here | -| `src/Simplex/Chat/Types.hs` | PC1 through PC31 | High | Core data types shared across all features | -| `src/Simplex/Chat/Core.hs` | PC1 through PC31 | High | Chat engine lifecycle (start, stop, subscribe) | -| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC31 | High | API command handler implementations | -| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC31 | High | Internal helpers for command processing | -| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC31 | High | Event subscriber — incoming message routing | +| `src/Simplex/Chat.hs` | PC1 through PC32 | High | Main chat module — top-level orchestration | +| `src/Simplex/Chat/Controller.hs` | PC1 through PC32 | High | Command processor — all API commands dispatched here | +| `src/Simplex/Chat/Types.hs` | PC1 through PC32 | High | Core data types shared across all features | +| `src/Simplex/Chat/Core.hs` | PC1 through PC32 | High | Chat engine lifecycle (start, stop, subscribe) | +| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC32 | High | API command handler implementations | +| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC32 | High | Internal helpers for command processing | +| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC32 | High | Event subscriber — incoming message routing | | `src/Simplex/Chat/Protocol.hs` | PC2, PC3, PC4, PC5, PC6, PC7 | High | Chat-level message protocol (x-events) | | `src/Simplex/Chat/Messages.hs` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9 | High | Message types and content | | `src/Simplex/Chat/Messages/CIContent.hs` | PC4, PC5, PC6, PC7, PC8, PC9, PC11 | Medium | Chat item content variants | @@ -496,8 +499,8 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | `src/Simplex/Chat/Files.hs` | PC10 | Medium | File transfer orchestration | | `src/Simplex/Chat/Delivery.hs` | PC2, PC3 | Medium | Message delivery engine | | `src/Simplex/Chat/Markdown.hs` | PC4 | Low | Markdown parsing for message formatting | -| `src/Simplex/Chat/Store.hs` | PC1 through PC31 | High | Database store interface | -| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC31 | Medium | Shared store utilities | +| `src/Simplex/Chat/Store.hs` | PC1 through PC32 | High | Database store interface | +| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC32 | Medium | Shared store utilities | | `src/Simplex/Chat/Store/Messages.hs` | PC4, PC5, PC6, PC7, PC8 | High | Message persistence | | `src/Simplex/Chat/Store/Groups.hs` | PC3, PC14, PC15, PC16, PC30 | High | Group persistence | | `src/Simplex/Chat/Store/Direct.hs` | PC2, PC12, PC13 | High | Contact persistence | @@ -526,11 +529,11 @@ The Haskell core is compiled as a shared native library (`libsimplex.so` / `libs | `src/Simplex/Chat/Operators/Presets.hs` | PC25 | Low | Preset server operators | | `src/Simplex/Chat/Operators/Conditions.hs` | PC25 | Low | Operator usage conditions | | `src/Simplex/Chat/AppSettings.hs` | PC25 | Low | App settings sync types | -| `src/Simplex/Chat/Mobile.hs` | PC1 through PC31 | High | C FFI exports — JNI bridge target | +| `src/Simplex/Chat/Mobile.hs` | PC1 through PC32 | High | C FFI exports — JNI bridge target | | `src/Simplex/Chat/Mobile/File.hs` | PC10 | Medium | Mobile file read/write FFI | -| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC31 | Medium | Shared FFI helpers | +| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC32 | Medium | Shared FFI helpers | | `src/Simplex/Chat/Mobile/WebRTC.hs` | PC17 | Low | WebRTC FFI helpers | -| `src/Simplex/Chat/View.hs` | PC1 through PC31 | Low | Terminal view rendering (not used by mobile/desktop UI) | +| `src/Simplex/Chat/View.hs` | PC1 through PC32 | Low | Terminal view rendering (not used by mobile/desktop UI) | | `src/Simplex/Chat/Stats.hs` | PC25 | Low | Server statistics tracking | | `src/Simplex/Chat/Util.hs` | — | Low | General Haskell utilities | | `src/Simplex/Chat/Styled.hs` | — | Low | Terminal styled text (not used by mobile/desktop UI) | diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md index 5003bb60e5..7f4ea08d90 100644 --- a/apps/multiplatform/spec/state.md +++ b/apps/multiplatform/spec/state.md @@ -451,6 +451,14 @@ val appPrefs: AppPreferences by lazy { AppPreferences() } | `oneHandUI` | `Boolean` | `true` | One-hand mode | | `chatBottomBar` | `Boolean` | `true` | Bottom bar in chat | +#### Hints + +Preferences listed in [`hintPreferences`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L279-L297) are restored to their defaults by "Reset all hints" (`resetHintPreferences()` in `views/usersettings/SettingsView.kt`). + +| Key | Type | Default | Purpose | +|---|---|---|---| +| [`directorySearchAlertShown`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L196) (`"DirectorySearchAlertShown"`) | `Boolean` | `false` | The user chose "Search and don't show again" in the online search alert; in `hintPreferences` (see [Chat List, Online Search](client/chat-list.md#11-online-search)) | + #### Remote Access (lines 238-243) | Key | Type | Default | Purpose |