From 4f3dabea263526737c9f599c071812abf985b787 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:46:52 +0000 Subject: [PATCH] ios: document online search in spec and product docs apps/ios/CODE.md asks every iOS change to keep spec/ and product/ in step with the code. The online search (SimpleX Directory and names, on the chat list and in New chat) had no documentation, including the parts added on this branch before: the spec gains a chat-list section and a New chat section, the API entries for the service request and the connection plan's failure mode, the impact and index rows and the Document Map rows for the new source files; the product docs gain the views, a concept row, rules for what the search may and may not do, and the known gaps. Only line references moved by this branch's own code are updated here. Run: 002-ios-directory-search-ui, review round 1 (R1-7) --- apps/ios/CODE.md | 3 + apps/ios/product/concepts.md | 1 + apps/ios/product/gaps.md | 22 ++++++ apps/ios/product/rules.md | 29 ++++++++ apps/ios/product/views/chat-list.md | 25 ++++++- apps/ios/product/views/new-chat.md | 21 +++++- apps/ios/spec/README.md | 4 +- apps/ios/spec/api.md | 25 ++++++- apps/ios/spec/client/chat-list.md | 110 +++++++++++++++++++++++++++- apps/ios/spec/client/navigation.md | 23 ++++++ apps/ios/spec/impact.md | 26 ++++--- apps/ios/spec/state.md | 11 +-- 12 files changed, 277 insertions(+), 23 deletions(-) diff --git a/apps/ios/CODE.md b/apps/ios/CODE.md index e6372a9e33..66a6405e71 100644 --- a/apps/ios/CODE.md +++ b/apps/ios/CODE.md @@ -167,6 +167,7 @@ After completing all changes (code + documentation), you MUST run an adversarial | Shared/AppDelegate.swift | spec/services/notifications.md | product/flows/onboarding.md | | Shared/Views/ChatList/ChatListView.swift | spec/client/chat-list.md | product/views/chat-list.md | | Shared/Views/ChatList/GetStakeBanner.swift | spec/client/chat-list.md | product/views/chat-list.md | +| Shared/Views/ChatList/DirectorySearchView.swift | spec/client/chat-list.md | product/views/chat-list.md, product/views/new-chat.md | | Shared/Views/Chat/ChatView.swift | spec/client/chat-view.md | product/views/chat.md | | Shared/Views/Chat/ComposeMessage/ComposeView.swift | spec/client/compose.md | product/views/chat.md | | Shared/Views/Chat/ChatItem/ | spec/client/chat-view.md | product/views/chat.md | @@ -179,6 +180,7 @@ After completing all changes (code + documentation), you MUST run an adversarial | Shared/Views/Chat/Group/ChannelRelaysView.swift | spec/client/chat-view.md | product/views/group-info.md | | Shared/Views/NewChat/NewChatView.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/NewChat/QRCode.swift | spec/client/navigation.md | product/views/new-chat.md | +| Shared/Views/NewChat/NewChatMenuButton.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/Call/ActiveCallView.swift | spec/services/calls.md | product/views/call.md | | Shared/Views/Call/CallController.swift | spec/services/calls.md | product/flows/calling.md | | Shared/Views/Call/WebRTCClient.swift | spec/services/calls.md | product/flows/calling.md | @@ -193,6 +195,7 @@ After completing all changes (code + documentation), you MUST run an adversarial | Shared/Model/ChatModel.swift | spec/state.md | product/concepts.md | | Shared/Model/SimpleXAPI.swift | spec/api.md, spec/architecture.md | product/concepts.md | | Shared/Model/AppAPITypes.swift | spec/api.md | product/concepts.md | +| Shared/Model/DirectorySearch.swift | spec/client/chat-list.md, spec/api.md | product/views/chat-list.md, product/rules.md | | Shared/Model/NtfManager.swift | spec/services/notifications.md | product/flows/messaging.md | | Shared/Model/BGManager.swift | spec/services/notifications.md | product/flows/messaging.md | | Shared/Theme/ThemeManager.swift | spec/services/theme.md | product/views/settings.md | diff --git a/apps/ios/product/concepts.md b/apps/ios/product/concepts.md index 89884eab43..89fb96903e 100644 --- a/apps/ios/product/concepts.md +++ b/apps/ios/product/concepts.md @@ -50,6 +50,7 @@ This document provides a structured mapping between product-level concepts, thei | 29 | User Address | [views/settings.md](views/settings.md) | [spec/api.md](../spec/api.md) | `UserSettings/UserAddressView.swift`, `Onboarding/AddressCreationCard.swift` | `Controller.hs` (`APICreateMyAddress`) | | 30 | Member Support Chat | [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md) | `Group/MemberSupportView.swift`, `MemberAdmissionView.swift` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | | 31 | Channels (Relays) | [glossary.md](glossary.md), [views/chat.md](views/chat.md), [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md), [spec/state.md](../spec/state.md), [spec/client/chat-view.md](../spec/client/chat-view.md), [spec/client/compose.md](../spec/client/compose.md) | `SimpleXChat/ChatTypes.swift` (`RelayStatus` incl. `.rsRejected`, `RelayStatus.text`, `GroupRelay`, `GroupMemberRole.relay`, `GroupMemberStatus.memRejected`, `CIDirection.channelRcv`, `GroupInfo.chatIconName`, `userCantSendReason`), `Shared/Views/Chat/ChatView.swift` (channel message rendering), `Shared/Views/Chat/ComposeMessage/ComposeView.swift` (`sendAsGroup`, Broadcast placeholder), `Shared/Views/Chat/Group/GroupChatInfoView.swift` (channel info adaptations), `Shared/Views/Chat/Group/GroupMemberInfoView.swift` (rejected-status row), `Shared/Views/Chat/Group/ChannelMembersView.swift`, `Shared/Views/Chat/Group/ChannelRelaysView.swift`, `Shared/Views/NewChat/AddChannelView.swift` (`relayStatusIndicator` rejected branch), `Shared/Model/AppAPITypes.swift` (`GroupShortLinkInfo`, `UserChatRelay`), `Shared/Model/SimpleXAPI.swift` (`apiNewPublicGroup`), `SimpleX SE/ShareAPI.swift` (channel `sendAsGroup`) | `Controller.hs` (`APINewPublicGroup`, `APIAllowRelayGroup`, `XGrpRelayReject` CONF handler) | +| 32 | 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-24 to RULE-28) | [spec/client/chat-list.md](../spec/client/chat-list.md) (section 11), [spec/client/navigation.md](../spec/client/navigation.md) (section 12), [spec/api.md](../spec/api.md) (sections 2.18, 8) | `Shared/Views/ChatList/DirectorySearchView.swift` (`OnlineSearchModel`, `NameSearchRow`, `DirectorySearchRow`, `confirmDirectorySearch`), `Shared/Model/DirectorySearch.swift` (`DirectorySearchEntry`, `simplexLinkType`, `planLocalChatId`, `NameSearchResult`, `OnlineSearchRows`), `Shared/Views/ChatList/ChatListView.swift` (`ChatListSearchBar` triggers, `nameRows`, `directoryRows`), `Shared/Views/NewChat/NewChatMenuButton.swift` (`NewChatSheet` online search, `ContactsList.shownChats`), `Shared/Model/SimpleXAPI.swift` (`apiSearchDirectory`, `apiConnectPlan` `onFailure`), `Shared/Model/ChatModel.swift` (`ConnectProgressOwner`), `SimpleXChat/APITypes.swift` (`JSONValue`) | `Controller.hs` (`APISendServiceRequest`, `APIConnectPlan`); directory service `../simplex-directory-service/src/Directory/Rpc.hs` | --- diff --git a/apps/ios/product/gaps.md b/apps/ios/product/gaps.md index 50d6bf2938..944cf06dfe 100644 --- a/apps/ios/product/gaps.md +++ b/apps/ios/product/gaps.md @@ -62,3 +62,25 @@ The Haskell Store modules (`Store/Direct.hs`, `Store/Groups.hs`, `Store/Messages --- +## Online Search + +### GAP: Groups joined through another link still appear as directory results +**Source:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) +A directory entry is matched to a local chat by a store-only lookup of its link. 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. + +**REC:** 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: First-search alert does not mention the server hosting a resolved address +**Source:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) +Resolving a SimpleX name with `PlanResolveMode.unknown` 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 alert names only SimpleX Directory and the name-resolving server. + +**REC:** Decide whether the alert copy should also say that the address a name points to is loaded. + +### GAP: New chat does not narrow contacts by name while typing +**Source:** [spec/client/navigation.md](../spec/client/navigation.md#12-new-chat-online-search) +The chat list resolves a typed SimpleX name locally on each keystroke to narrow the list to the matching chat; New chat does not. 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). + +**REC:** Reuse the chat list's local name resolution in `ContactsListSearchBar` if this proves confusing. + +--- + diff --git a/apps/ios/product/rules.md b/apps/ios/product/rules.md index 0cb3f8e96a..8ea269bfb7 100644 --- a/apps/ios/product/rules.md +++ b/apps/ios/product/rules.md @@ -146,3 +146,32 @@ **Rule:** CallKit MUST be disabled in regions where it is restricted (China). The app uses in-app call UI as fallback. **Enforced by:** `CallController.swift` checks `useCallKit()` based on region; `ActiveCallView.swift` provides fallback UI. **Spec:** [spec/services/calls.md](../spec/services/calls.md) + +--- + +## Online Search + +### RULE-24: Online results are never saved +**Rule:** 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. +**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). +**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 +**Rule:** 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. +**Enforced by:** `DirectorySearch.swift` (`DirectorySearchEntry.init(from:)` with `simplexLinkType`; `parseDirectorySearchResponse` decodes entries one at a time). +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +### RULE-26: Online lookups never show alerts +**Rule:** 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. +**Enforced by:** `SimpleXAPI.swift` (`apiConnectPlan(onFailure: .log)`); `DirectorySearchView.swift` (`inProgress: BoxedValue(false)` for every lookup). +**Spec:** [spec/api.md](../spec/api.md#8-directory-search-and-connection-plans) + +### RULE-27: Nothing is shown twice +**Rule:** 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. +**Enforced by:** `DirectorySearchView.swift` (`OnlineSearchModel.rows(shownChatIds:)`); `DirectorySearch.swift` (`planLocalChatId`, `NameSearchResult.resolvedLink`). +**Spec:** [spec/client/chat-list.md](../spec/client/chat-list.md#11-online-search) + +### RULE-28: Consent before the first online search +**Rule:** 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. +**Enforced by:** `DirectorySearchView.swift` (`confirmDirectorySearch`, `DEFAULT_DIRECTORY_SEARCH_ALERT_SHOWN`); both triggers in `ChatListView.swift` and `NewChatMenuButton.swift` go through it. +**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 dee9e1d427..0da1b7d036 100644 --- a/apps/ios/product/views/chat-list.md +++ b/apps/ios/product/views/chat-list.md @@ -33,6 +33,24 @@ The toolbar supports two layout modes: - When active, `searchMode = true` hides the navigation bar and shows inline search - Filters chat list in real-time by contact/group name and message content - Detects pasted SimpleX links (`searchShowingSimplexLink`) and offers to connect +- For non-empty text that is not a link, offers "Search in Directory"; the keyboard Search key 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. + +| Element | Behavior | +|---|---| +| "Search in Directory" row | Shown for non-empty text that is not a SimpleX link. With the one-hand UI it takes the place of the tags above the search field; with the top bar it is below the field. Searches SimpleX Directory for public groups and channels | +| Keyboard Search key | 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; 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. Cancel / Search / Search and don't show again ("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 online results replace the onboarding pages while a search runs or has results. ### Chat Filter Tabs (Tags) @@ -106,7 +124,10 @@ Triggers `reconnectAllServers()` after user confirmation alert ("Reconnect serve | Chat database not started | Settings row shows exclamation icon; chat running == false disables interactions | | No conversations yet | `ConnectOnboardingView` pages replace the list, with the crowdfunding banner below them where it applies | | Connection in progress | `ConnectProgressManager` overlay with connecting text | -| Search with no results | Empty list with no special empty-state view | +| Search with no results | "No chats found" in the middle of the list, unless an online search is running or shows results | +| Online search running | Progress indicator in the search field (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) | ## Related Specs @@ -130,3 +151,5 @@ Triggers `reconnectAllServers()` after user confirmation alert ("Reconnect serve - `Shared/Views/ChatList/OneHandUICard.swift` -- One-hand UI introduction card - `Shared/Views/ChatList/GetStakeBanner.swift` -- Wefunder crowdfunding banner and shared banner card chrome - `Shared/Views/ChatList/ServersSummaryView.swift` -- Server subscription summary +- `Shared/Views/ChatList/DirectorySearchView.swift` -- Online search model, Names and Directory rows, first-search alert +- `Shared/Model/DirectorySearch.swift` -- Directory response parsing and online result types diff --git a/apps/ios/product/views/new-chat.md b/apps/ios/product/views/new-chat.md index 0d1e384325..caf89ed699 100644 --- a/apps/ios/product/views/new-chat.md +++ b/apps/ios/product/views/new-chat.md @@ -69,6 +69,20 @@ Accessed via `NewChatMenuButton` dropdown: | Incognito option | Create group with random profile | | Create button | Creates group via API and navigates to group chat | +### Search (New Chat Sheet) + +The search field at the top 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 for non-empty text that is not a SimpleX link; searches SimpleX Directory | +| Keyboard Search key | Searches the directory and, for a SimpleX name, resolves the name online | +| Names and Directory sections | After "Your Contacts", with headers in the same style. Tapping a result connects and closes the sheet, as other New chat actions do. A result for a contact already listed in "Your Contacts" is not repeated | +| Contacts empty state | "No filtered contacts" / "No contacts" are not shown while the online search runs or shows results | + +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. Archived contacts keep their own search field without the Search key or online results. + ## Loading / Error States | State | Behavior | @@ -78,6 +92,8 @@ Accessed via `NewChatMenuButton` dropdown: | Invalid link pasted | Alert shown via `NewChatViewAlert.newChatSomeAlert` | | Connection in progress | Chat list shows pending connection entry | | Unused invitation on dismiss | Alert: "Keep unused invitation?" with Keep/Delete options | +| Online search running | Progress indicator in the search field; closing the sheet cancels the search | +| Directory search failed | "Search failed, tap to retry" row at the end of the Directory section | ## Create Channel (`AddChannelView`) @@ -138,14 +154,15 @@ When preparing a channel link, `groupShortLinkInfo.groupRelays` (hostnames) are ## Related Specs - `spec/api.md` -- API commands: `APIAddContact`, `APIConnect`, `APICreateUserAddress` -- `spec/client/navigation.md` -- Navigation architecture for channel creation flow +- `spec/client/navigation.md` -- Navigation architecture for channel creation flow; New chat online search (section 12) +- `spec/client/chat-list.md` -- Online search model and rows (section 11) - [Chat List](chat-list.md) -- Parent view that presents this sheet - [Chat](chat.md) -- Navigated to after successful connection ## Source Files - `Shared/Views/NewChat/NewChatView.swift` -- Main view with invite/connect tabs, link generation -- `Shared/Views/NewChat/NewChatMenuButton.swift` -- Dropdown menu (new chat, create group, create channel) +- `Shared/Views/NewChat/NewChatMenuButton.swift` -- Dropdown menu (new chat, create group, create channel); New chat sheet search, contacts list and online search sections - `Shared/Views/NewChat/QRCode.swift` -- QR code generation and display - `Shared/Views/NewChat/AddGroupView.swift` -- Group creation form - `Shared/Views/NewChat/AddChannelView.swift` -- Channel creation wizard (3 steps) diff --git a/apps/ios/spec/README.md b/apps/ios/spec/README.md index eca6103582..a14fc3b4e7 100644 --- a/apps/ios/spec/README.md +++ b/apps/ios/spec/README.md @@ -40,9 +40,9 @@ SimpleXApp (root entry point) | [State Management](state.md) | ChatModel, ItemsModel, Chat, ChatInfo, preference storage | | [Database & Storage](database.md) | SQLite databases, encryption, file storage, export/import | | [Chat View](client/chat-view.md) | Message rendering, chat item types, context menu actions | -| [Chat List](client/chat-list.md) | Conversation list, filtering, search, swipe actions | +| [Chat List](client/chat-list.md) | Conversation list, filtering, search, swipe actions, online search (SimpleX Directory and names) | | [Message Composition](client/compose.md) | Compose bar, attachments, reply/edit/forward modes, voice recording | -| [Navigation](client/navigation.md) | Navigation stack, deep linking, sheet presentation, call overlay | +| [Navigation](client/navigation.md) | Navigation stack, deep linking, sheet presentation, call overlay, New chat online search | | [Push Notifications](services/notifications.md) | NtfManager, NSE, notification modes, token lifecycle | | [WebRTC Calling](services/calls.md) | CallController, WebRTCClient, CallKit, signaling via SMP | | [File Transfer](services/files.md) | Inline/XFTP transfer, auto-receive, CryptoFile, file constants | diff --git a/apps/ios/spec/api.md b/apps/ios/spec/api.md index 56c3c34cb6..e7ac2f66ad 100644 --- a/apps/ios/spec/api.md +++ b/apps/ios/spec/api.md @@ -18,6 +18,7 @@ 5. [Error Types (ChatError)](#5-error-types) 6. [FFI Bridge Functions](#6-ffi-bridge-functions) 7. [Result Type (APIResult)](#7-result-type) +8. [Directory Search and Connection Plans](#8-directory-search-and-connection-plans) --- @@ -290,6 +291,12 @@ The `ChatCommand` enum ([`AppAPITypes.swift` L15](../Shared/Model/AppAPITypes.sw | `apiSyncContactRatchet` | `contactId, force` | Sync double-ratchet state | [L120](../Shared/Model/AppAPITypes.swift#L120) | | `apiSyncGroupMemberRatchet` | `groupId, groupMemberId, force` | Sync member ratchet | [L121](../Shared/Model/AppAPITypes.swift#L121) | +### 2.18 Service Requests + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiSendServiceRequest` | `userId, target, timeoutSec: Double?, requestJSON` | Send a JSON request to a service address (e.g. SimpleX Directory) and wait up to the timeout for its response (`/_service_request`); returns `serviceResponse` | [L137](../Shared/Model/AppAPITypes.swift#L137) | + --- ## 3. Response Types @@ -327,6 +334,7 @@ Contact, message, and profile responses ([`AppAPITypes.swift` L779](../Shared/Mo |----------|-----------|-------------|--------| | `invitation` | `user, connLinkInvitation, connection` | Created invitation link | [L780](../Shared/Model/AppAPITypes.swift#L780) | | `connectionPlan` | `user, connLink, connectionPlan` | Connection plan preview | [L783](../Shared/Model/AppAPITypes.swift#L783) | +| `serviceResponse` | `user, responseData: JSONValue` | A service's response to `apiSendServiceRequest`, left undecoded for the caller | [L862](../Shared/Model/AppAPITypes.swift#L862) | | `newPreparedChat` | `user, chat: ChatData` | Prepared contact/group | [L784](../Shared/Model/AppAPITypes.swift#L784) | | `startedConnectionToGroup` | `user, groupInfo, relayResults: [RelayConnectionResult]` | Group/channel join initiated; relay results indicate per-relay connection success/failure | [L790](../Shared/Model/AppAPITypes.swift#L790) | | `contactDeleted` | `user, contact` | Contact deleted | [L793](../Shared/Model/AppAPITypes.swift#L793) | @@ -479,7 +487,7 @@ public enum ChatError: Decodable, Hashable, Error { | Category | Enum | Description | Source | |----------|------|-------------|--------| -| Chat logic | `ChatErrorType` | Business logic errors (e.g., invalid state, permission denied, `chatRelayExists`) | [`APITypes.swift` L722](../SimpleXChat/APITypes.swift#L722) | +| Chat logic | `ChatErrorType` | Business logic errors (e.g., invalid state, permission denied, `chatRelayExists`) | [`APITypes.swift` L788](../SimpleXChat/APITypes.swift#L788) | | SMP Agent | `AgentErrorType` | Protocol/network errors from the SMP agent layer | [`APITypes.swift` L884](../SimpleXChat/APITypes.swift#L884) | | Database store | `StoreError` | SQLite query/constraint errors (includes relay-related: `relayUserNotFound`, `duplicateMemberId`, `userChatRelayNotFound`, `groupRelayNotFound`, `groupRelayNotFoundByMemberId`) | [`APITypes.swift` L802](../SimpleXChat/APITypes.swift#L802) | | Database engine | `DatabaseError` | DB open/migration/encryption errors | [`APITypes.swift` L871](../SimpleXChat/APITypes.swift#L871) | @@ -596,6 +604,20 @@ The `decodeAPIResult` function ([`APITypes.swift` L86](../SimpleXChat/APIType --- +## 8. Directory Search and Connection Plans + +Wrappers in [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) used by the online search (see [Chat List, Online Search](client/chat-list.md#11-online-search)): + +| Function | Description | Source | +|----------|-------------|--------| +| `apiSearchDirectory(_ text:, cursor:)` | Sends `apiSendServiceRequest` to `DIRECTORY_SERVICE_LINK` with `DIRECTORY_SEARCH_TIMEOUT_SEC` and the search request; returns `parseDirectorySearchResponse(responseData)`, or nil on any error (logged). Blocks for up to the timeout | [L1044](../Shared/Model/SimpleXAPI.swift#L1044) | +| `apiConnectPlan(connLink:, resolveMode:, linkOwnerSig:, inProgress:, onFailure:)` | Plans a connection; returns `ConnectionPlanResult?`. `notResolvedLocally` (a `.never` lookup that matched nothing) returns nil silently; any other failure is handled by `onFailure` | [L1064](../Shared/Model/SimpleXAPI.swift#L1064) | +| `ConnectPlanFailure` | `.alert` (default, every existing caller): `apiConnectResponseAlert`; `.log`: debug log only, used by the online search so name resolution and per-entry lookups never raise alerts | [L1060](../Shared/Model/SimpleXAPI.swift#L1060) | + +A service response is carried as [`JSONValue`](../SimpleXChat/APITypes.swift#L722) (any JSON), so the framework does not depend on a service's own types; callers decode it with [`decodeJSONValue`](../SimpleXChat/APITypes.swift#L760). + +--- + ## Source Files | File | Path | @@ -605,6 +627,7 @@ The `decodeAPIResult` function ([`APITypes.swift` L86](../SimpleXChat/APIType | ChatEvent enum | [`Shared/Model/AppAPITypes.swift` L1069](../Shared/Model/AppAPITypes.swift#L1069) | | APIResult, ChatError | [`SimpleXChat/APITypes.swift` L27, L699](../SimpleXChat/APITypes.swift#L27) | | FFI bridge functions | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) | +| Service response JSON | [`SimpleXChat/APITypes.swift` L722](../SimpleXChat/APITypes.swift#L722) | | Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift) | | Data types | `SimpleXChat/ChatTypes.swift` | | C header | `SimpleXChat/SimpleX.h` | diff --git a/apps/ios/spec/client/chat-list.md b/apps/ios/spec/client/chat-list.md index 2087d0dee3..a90a71fb13 100644 --- a/apps/ios/spec/client/chat-list.md +++ b/apps/ios/spec/client/chat-list.md @@ -5,7 +5,7 @@ > Related specs: [Chat View](chat-view.md) | [Navigation](navigation.md) | [State Management](../state.md) | [README](../README.md) > Related product: [Chat List View](../../product/views/chat-list.md) -**Source:** [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) +**Source:** [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) | [`DirectorySearchView.swift`](../../Shared/Views/ChatList/DirectorySearchView.swift) | [`DirectorySearch.swift`](../../Shared/Model/DirectorySearch.swift) --- @@ -21,6 +21,7 @@ 8. [UserPicker](#8-userpicker) 9. [Floating Action Button](#9-floating-action-button) 10. [Crowdfunding Banner](#10-crowdfunding-banner) +11. [Online Search](#11-online-search) --- @@ -226,6 +227,7 @@ Search is available via pull-down gesture or search button in the navigation bar - Matching chats are displayed in the same list format - Results update as the user types (debounced) - Clearing search restores the full filtered list +- Searching SimpleX Directory and resolving SimpleX names online happen only on request, below the local results: see [Online Search](#11-online-search) --- @@ -318,6 +320,110 @@ Tapping the card opens [`GetStakeView`](../../Shared/Views/Onboarding/WhatsNewVi --- +## 11. Online Search {#11-online-search} + +**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, 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`](../../Shared/Views/ChatList/ChatListView.swift#L843) calls `reset()` on every change of `searchText` ([L932](../../Shared/Views/ChatList/ChatListView.swift#L932)) and of `currentUser?.userId` ([L935](../../Shared/Views/ChatList/ChatListView.swift#L935)). The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#12-new-chat-online-search)). + +### Triggers + +| Trigger | Handler | Runs | +|---------|---------|------| +| Keyboard Search key (`.submitLabel(.search)`) | [`runOnlineSearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1043) | `search(text, name: connectNameCandidate)`: the directory's first page, and name resolution when the text is a name candidate | +| "Search in Directory" row ([`SearchInDirectoryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L174)) | [`runDirectorySearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1037) | `searchDirectory(text)`: the directory only | + +Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link, and both go through [`confirmDirectorySearch()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L305), which runs the search at once when `DEFAULT_DIRECTORY_SEARCH_ALERT_SHOWN` is set and otherwise shows the three-action alert ([`showDirectorySearchAlert`](../../Shared/Views/ChatList/DirectorySearchView.swift#L313)): Cancel / Search / Search and don't show again. Its message names both recipients: the directory, and, for a SimpleX name, the server that resolves names. The key is in `hintDefaults`, so "Reset all hints" brings the alert back. + +The rows above or below the search field follow the toolbar mode: + +| Search text | One-hand UI (slot above the field, [L865](../../Shared/Views/ChatList/ChatListView.swift#L865)) | Top bar (below the field, [L916](../../Shared/Views/ChatList/ChatListView.swift#L916)) | +|-------------|-----------------------------------|------------------------------| +| Empty | Tags | Tags above; nothing below | +| Plain text | "Search in Directory" | Tags above; "Search in Directory" below | +| Name candidate, no name results yet | "Connect to …" and "Search in Directory" | Tags above; "Connect to …" and "Search in Directory" below | +| Name candidate with name results | "Search in Directory" | Tags above; "Search in Directory" below | +| Pasted SimpleX link | Tags | Tags above; nothing below | + +### [`OnlineSearchModel`](../../Shared/Views/ChatList/DirectorySearchView.swift#L18-L172) + +`@MainActor` `ObservableObject`, owned by `ChatListView` as `@StateObject onlineSearch` and passed to `ChatListSearchBar`. + +| State | Description | +|-------|-------------| +| `names: [NameSearchResult]` | Name results for the current text | +| `directory: [DirectoryResult]` | 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 `JSONValue` from the last page, echoed back for the next one; nil when there are no more | +| `generation` (private) | Bumped by `reset()` and `cancelRequest()`; a reply whose captured generation differs never writes model state | + +| Computed | Definition | +|----------|------------| +| [`searching`](../../Shared/Views/ChatList/DirectorySearchView.swift#L33) | `loading \|\| resolvingNames` | +| [`showResults`](../../Shared/Views/ChatList/DirectorySearchView.swift#L34) | `searching \|\| searched \|\| !names.isEmpty` | +| [`footer`](../../Shared/Views/ChatList/DirectorySearchView.swift#L35) | `DirectoryFooter.retry` when `failed`, else `.showMore` when a cursor exists, else nil | + +| Method | Description | +|--------|-------------| +| [`reset()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L37) | Clears all results and flags, bumps `generation` (text change, profile switch) | +| [`cancelRequest()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L50) | The spinner's `onCancel`: bumps `generation` and clears `loading` and `resolvingNames`; results already shown stay | +| [`search(_:name:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L56) | 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 (`async let`, with the generation captured before the child task starts) | +| [`searchDirectory(_:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L73) | 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()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L88) | "Show more": the next page, appended | +| [`retry()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L94) | The retry row: resumes the page when a cursor exists, else repeats the first page; no reset | +| [`rows(shownChatIds:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L102) | The rows to render, given the ids of the local chats shown (see De-duplication) | + +**Spinner.** `loading` and `resolvingNames` have a `didSet` that calls [`updateSpinner()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L158), which starts the "Searching directory…" progress with owner `.directorySearch` when `searching` becomes true and stops it when it becomes false (`spinnerShown` keeps the last state, so repeated writes do nothing). The search field shows it as a spinner once it has run for a second. The spinner is shared with connections: [`ConnectProgressOwner`](../../Shared/Model/ChatModel.swift#L303) records whose it is, [`startConnectProgress`](../../Shared/Model/ChatModel.swift#L316) does not let a search take it from a running connection, and [`stopConnectProgress`](../../Shared/Model/ChatModel.swift#L327) ignores a stop from another owner. Tapping a result calls `planAndConnect`, whose `cancelConnectProgress()` ([L336](../../Shared/Model/ChatModel.swift#L336)) cancels the search through its `onCancel` before the connection takes the spinner. + +### Directory requests + +[`request(append:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L133) calls [`apiSearchDirectory`](../../Shared/Model/SimpleXAPI.swift#L1044), which sends `APISendServiceRequest` to [`DIRECTORY_SERVICE_LINK`](../../Shared/Model/DirectorySearch.swift#L14) with a [10-second timeout](../../Shared/Model/DirectorySearch.swift#L17) and the request built by [`directorySearchRequestJSON`](../../Shared/Model/DirectorySearch.swift#L84) (`searchText`, and `searchCursor` for later pages). [`parseDirectorySearchResponse`](../../Shared/Model/DirectorySearch.swift#L92) accepts only a `searchResults` response and decodes its entries one at a time, so an entry the app cannot decode is dropped and the rest of the page is kept. + +[`DirectorySearchEntry`](../../Shared/Model/DirectorySearch.swift#L29-L64) has a custom [`init(from:)`](../../Shared/Model/DirectorySearch.swift#L46): its `link` is the wire `groupLink.connShortLink`, else `connFullLink`, and decoding throws unless [`simplexLinkType`](../../Shared/Model/DirectorySearch.swift#L66) classifies it as `.group` or `.channel`. `simplexLinkType` returns the link type only when [`strConnectTarget`](../../Shared/Views/NewChat/NewChatView.swift#L860) 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(connLink: entry.link, resolveMode: .never, onFailure: .log)` and [`planLocalChatId`](../../Shared/Model/DirectorySearch.swift#L102), building a [`DirectoryResult`](../../Shared/Model/DirectorySearch.swift#L173). The link-type check makes this lookup store-only: the core plans contact-mode links under `.never` 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(_:generation:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L117) resolves `[name]` for a candidate starting with `@` or `#`, else `["@" + name, "#" + name]` (the candidate comes from `nameSearchCandidate`, as on the typing path). Each target is planned with `apiConnectPlan(connLink: target, resolveMode: .unknown, onFailure: .log)`; it stops before a target once the generation changed, so a cancelled or replaced search sends no further names. [`NameSearchResult.init?(target:result:)`](../../Shared/Model/DirectorySearch.swift#L138) maps the plan with one exhaustive switch: 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`, 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; `inProgress: BoxedValue(false)` also suppresses the network retry alert. + +### De-duplication + +[`rows(shownChatIds:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L102) returns an [`OnlineSearchRows`](../../Shared/Model/DirectorySearch.swift#L180) at render 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`. + +### Rendering + +| Where | Rows | Empty state | +|-------|------|-------------| +| [`chatListContent`](../../Shared/Views/ChatList/ChatListView.swift#L533) | `rows(shownChatIds:)` with the ids of the shown chats ([L535](../../Shared/Views/ChatList/ChatListView.swift#L535)); after the local chats, [`nameRows`](../../Shared/Views/ChatList/ChatListView.swift#L483) then [`directoryRows`](../../Shared/Views/ChatList/ChatListView.swift#L498) | `noChatsView()` overlay only when no chat is shown, the model is not `searching` and `rows.isEmpty` ([L667](../../Shared/Views/ChatList/ChatListView.swift#L667)) | +| Onboarding branch of [`chatList`](../../Shared/Views/ChatList/ChatListView.swift#L434) | While `showResults`, a list of the same parts with `shownChatIds: []` ([L449](../../Shared/Views/ChatList/ChatListView.swift#L449)) in place of the onboarding pages | A centred "No chats found" row when not searching and `rows.isEmpty` | + +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. Each starts with a header row ([`searchPartHeader`](../../Shared/Views/ChatList/ChatListView.swift#L518): callout font, secondary colour, `.isHeader` trait, no separator) rather than a sticky `Section` header, since the one-hand list is flipped. Every row carries the per-row flip and a clear background. + +| Row | View | Tap | +|-----|------|-----| +| Name | [`NameSearchRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L249): avatar, profile name in bold, the SimpleX name in parentheses (secondary), description (2 lines) | `planAndConnect(result.target, dismiss: false)` | +| Directory entry | [`DirectorySearchRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L196): avatar, `#` + name, the directory's SimpleX name claim as plain text, description (2 lines), member or subscriber count, and the verb "Join channel" / "Join group" (one line, fixed size) | `planAndConnect(result.entry.link, dismiss: false)` | +| Retry | [`DirectoryRetryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L291) via [`directoryRetryRow()`](../../Shared/Views/ChatList/ChatListView.swift#L681): "Search failed, tap to retry" | `retry()` | +| Show more | [`DirectoryShowMoreRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L276) via [`directoryShowMoreRow()`](../../Shared/Views/ChatList/ChatListView.swift#L675): "Show more", disabled while `loading` | `loadMore()` | + +A name or directory row tap first sets `searchFocussed = false`. Text from the directory and from name owners is rendered with `Text(String)` / `Text(verbatim:)` (never parsed as markdown) with line limits. + +[GAP] Resolving a SimpleX name with `.unknown` 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. + +--- + ## Source Files | File | Path | Key struct | Line | @@ -334,3 +440,5 @@ Tapping the card opens [`GetStakeView`](../../Shared/Views/Onboarding/WhatsNewVi | Server summary | [`ServersSummaryView.swift`](../../Shared/Views/ChatList/ServersSummaryView.swift) | | | | One-hand UI card | [`OneHandUICard.swift`](../../Shared/Views/ChatList/OneHandUICard.swift) | | | | Crowdfunding banner | [`GetStakeBanner.swift`](../../Shared/Views/ChatList/GetStakeBanner.swift) | `GetStakeBanner` | [13](../../Shared/Views/ChatList/GetStakeBanner.swift#L13) | +| Online search model and rows | [`DirectorySearchView.swift`](../../Shared/Views/ChatList/DirectorySearchView.swift) | `OnlineSearchModel` | [18](../../Shared/Views/ChatList/DirectorySearchView.swift#L18) | +| Directory response and online result types | [`DirectorySearch.swift`](../../Shared/Model/DirectorySearch.swift) | `DirectorySearchEntry` | [29](../../Shared/Model/DirectorySearch.swift#L29) | diff --git a/apps/ios/spec/client/navigation.md b/apps/ios/spec/client/navigation.md index 64d0940e39..9aee1bfe87 100644 --- a/apps/ios/spec/client/navigation.md +++ b/apps/ios/spec/client/navigation.md @@ -357,6 +357,29 @@ For channels: shows "This is your link for channel" with only "Open channel" + " For channels (`groupInfo.useRelays`): the title is "Open channel"; for groups, "Open group"; business chats keep "Open chat" / "Open new chat". Unless the chat is merely prepared (`nextConnectPrepared`) or a business chat, the alert shows an information line with the user's membership role (`memberRoleInformation`) in the secondary color: subscriber/contributor for channels, observer/member for groups, moderator/admin/owner for both. +## 12. New Chat Online Search + +**Source:** [`Shared/Views/NewChat/NewChatMenuButton.swift`](../../Shared/Views/NewChat/NewChatMenuButton.swift) + +[`NewChatSheet`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L36) offers the chat list's online search (see [Chat List, Online Search](chat-list.md#11-online-search)) with its own [`OnlineSearchModel`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L46), reset on every change of `searchText` ([L187](../../Shared/Views/NewChat/NewChatMenuButton.swift#L187)) and of `currentUser?.userId` ([L190](../../Shared/Views/NewChat/NewChatMenuButton.swift#L190)). The sheet's `onDisappear` calls `cancelConnectProgress()`, which cancels a search still in flight. + +| Element | Where | Behavior | +|---------|-------|----------| +| Search key | [`ContactsListSearchBar`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L435) with `onSearch` ([L445](../../Shared/Views/NewChat/NewChatMenuButton.swift#L445)) | `.submitLabel(.search)` and `.onSubmit` run [`runOnlineSearch()`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L245): `search(text, name: connectNameCandidate)` | +| "Connect to …" row | below the field ([L99](../../Shared/Views/NewChat/NewChatMenuButton.swift#L99)) | shown for a name candidate until name results arrive | +| "Search in Directory" row | below the field ([L109](../../Shared/Views/NewChat/NewChatMenuButton.swift#L109)) | shown for non-empty, non-link text; runs [`runDirectorySearch()`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L239): `searchDirectory(text)` | +| Names and Directory sections | after `ContactsList`, [`onlineSearchSections(_:)`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L195) | `Section`s with headers styled as "Your Contacts" ([`onlineSearchHeader`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L229)); the Directory section ends with the retry or "Show more" row; rows tap `planAndConnect(…, dismiss: true)` | + +Both triggers do nothing for empty text or a pasted link and go through `confirmDirectorySearch`. The list is `insetGrouped` and not flipped, so the sections use ordinary `Section` headers and default row backgrounds. + +**De-duplication against the contacts list.** Before `ContactsList(…)` the list builder computes [`ContactsList.shownChats(…)`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L392) with the same inputs `ContactsList` gets (`contactListChatPredicate`, the search state, `DEFAULT_SHOW_UNREAD_AND_FAVORITES`) and passes the ids of the shown contacts to `rows(shownChatIds:)` ([L173](../../Shared/Views/NewChat/NewChatMenuButton.swift#L173)). `shownChats` returns the contacts matching the predicate and the filtered, sorted list the section shows; `ContactsList.body` uses the same function. + +**Empty state.** [`ContactsList`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L303) has `showsEmptyState` (default `true`, [L314](../../Shared/Views/NewChat/NewChatMenuButton.swift#L314)); the sheet passes `!onlineSearch.searching && rows.isEmpty`, so "No filtered contacts" / "No contacts" are omitted while the online search runs or shows rows. + +**Archived contacts.** [`DeletedChats`](../../Shared/Views/NewChat/NewChatMenuButton.swift#L563) passes neither `onSearch` nor `showsEmptyState`: its search field keeps the `.return` label with a no-op submit, and its empty states are unchanged. + +[GAP] New chat does not resolve a typed name locally while typing, 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. + --- ## Source Files diff --git a/apps/ios/spec/impact.md b/apps/ios/spec/impact.md index 35528f30d6..b4fb594f14 100644 --- a/apps/ios/spec/impact.md +++ b/apps/ios/spec/impact.md @@ -41,6 +41,7 @@ | PC29 | User Address | | PC30 | Member Support Chat | | PC31 | Channels (Relays) | +| PC32 | Online Search (Directory and Names) | --- @@ -49,10 +50,11 @@ | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| | Shared/ContentView.swift | PC1, PC2, PC3 | High | Root navigation — affects all chat access | -| Shared/SimpleXApp.swift | PC1 through PC31 | High | App entry point — initialization affects everything | +| Shared/SimpleXApp.swift | PC1 through PC32 | High | App entry point — initialization affects everything | | Shared/AppDelegate.swift | PC18 | Medium | Push notification registration | -| Shared/Views/ChatList/ChatListView.swift | PC1, PC28 | High | Main screen rendering and filtering | +| Shared/Views/ChatList/ChatListView.swift | PC1, PC28, PC32 | High | Main screen rendering and filtering; online search triggers and result parts | | Shared/Views/ChatList/GetStakeBanner.swift | PC1 | Low | Crowdfunding banner and shared banner card chrome | +| Shared/Views/ChatList/DirectorySearchView.swift | PC1, PC32 | Medium | Online search model (generation, spinner, de-duplication), result rows, consent alert | | Shared/Views/Chat/ChatView.swift | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11, PC31 | High | Core conversation UI — most messaging features, channel message rendering | | Shared/Views/Chat/ComposeMessage/ComposeView.swift | PC4, PC6, PC9, PC11, PC31 | High | Message composition — send path for all messages, channel sendAsGroup | | Shared/Views/Chat/ChatItem/ | PC2, PC3, PC5, PC7, PC8, PC9, PC10, PC11 | Medium | Individual message rendering components | @@ -65,10 +67,11 @@ | Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30, PC31 | Medium | Member details and role management; rejected-by-operator status row for relay members | | Shared/Views/NewChat/NewChatView.swift | PC12, PC31 | High | New connection creation — onramp for all contacts and channels | | Shared/Views/NewChat/QRCode.swift | PC12 | Low | QR code display/scanning utility | +| Shared/Views/NewChat/NewChatMenuButton.swift | PC12, PC14, PC31, PC32 | Medium | New chat sheet: actions, contacts list, online search sections; contacts list shared with archived contacts | | Shared/Views/Call/ActiveCallView.swift | PC17 | Medium | Call UI rendering | | Shared/Views/Call/CallController.swift | PC17 | High | CallKit integration — call lifecycle | | Shared/Views/Call/WebRTCClient.swift | PC17 | High | WebRTC session management | -| Shared/Views/UserSettings/SettingsView.swift | PC18, PC22, PC23, PC24, PC25, PC29 | Medium | Settings navigation hub | +| Shared/Views/UserSettings/SettingsView.swift | PC18, PC22, PC23, PC24, PC25, PC29, PC32 | Medium | Settings navigation hub; app defaults and hint defaults (incl. the online search alert) | | Shared/Views/UserSettings/AppearanceSettings.swift | PC24 | Low | Theme customization UI | | Shared/Views/UserSettings/NetworkAndServers/ | PC25, PC31 | High | Server configuration — affects connectivity and relay validation | | Shared/Views/UserSettings/UserProfilesView.swift | PC19, PC21 | Medium | Profile management | @@ -76,14 +79,15 @@ | Shared/Views/LocalAuth/ | PC22 | Medium | App lock functionality | | Shared/Views/Database/ | PC23, PC26 | High | Database encryption and export | | Shared/Views/Migration/ | PC26 | High | Device migration — data portability | -| Shared/Model/ChatModel.swift | PC1 through PC31 | High | Central state — all features depend on it | -| Shared/Model/SimpleXAPI.swift | PC1 through PC31 | High | FFI bridge — all commands flow through here | -| Shared/Model/AppAPITypes.swift | PC1 through PC31 | High | Command/response types — all API communication | +| Shared/Model/ChatModel.swift | PC1 through PC32 | High | Central state — all features depend on it | +| Shared/Model/SimpleXAPI.swift | PC1 through PC32 | High | FFI bridge — all commands flow through here | +| Shared/Model/AppAPITypes.swift | PC1 through PC32 | High | Command/response types — all API communication | +| Shared/Model/DirectorySearch.swift | PC32 | High | Directory response parsing and link-type validation of untrusted entries; online result types | | Shared/Model/NtfManager.swift | PC18 | High | Notification delivery | | Shared/Model/BGManager.swift | PC18 | Medium | Background fetch scheduling | | Shared/Theme/ThemeManager.swift | PC24 | Medium | Theme resolution engine | -| SimpleXChat/ChatTypes.swift | PC1 through PC31 | High | Core data types — all features use them | -| SimpleXChat/APITypes.swift | PC1 through PC31 | High | API result types and error handling | +| SimpleXChat/ChatTypes.swift | PC1 through PC32 | High | Core data types — all features use them | +| SimpleXChat/APITypes.swift | PC1 through PC32 | High | API result types and error handling | | SimpleXChat/CallTypes.swift | PC17 | Medium | Call-specific data types | | SimpleXChat/FileUtils.swift | PC10, PC23, PC26 | Medium | File paths and encryption utilities | | SimpleXChat/Notifications.swift | PC18 | Medium | Notification type definitions | @@ -97,9 +101,9 @@ | Source File | Product Concepts Affected | Risk Level | Notes | |-------------|--------------------------|------------|-------| -| src/Simplex/Chat/Controller.hs | PC1 through PC31 | High | Command processor — all API commands | -| 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 | +| src/Simplex/Chat/Controller.hs | PC1 through PC32 | High | Command processor — all API commands | +| 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 | | 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 | diff --git a/apps/ios/spec/state.md b/apps/ios/spec/state.md index 88f798666c..91111f29f1 100644 --- a/apps/ios/spec/state.md +++ b/apps/ios/spec/state.md @@ -271,11 +271,11 @@ enum ActiveFilter { --- -## 5. [ChannelRelaysModel](../Shared/Model/ChatModel.swift#L336-L350) +## 5. [ChannelRelaysModel](../Shared/Model/ChatModel.swift#L354-L368) **Class**: `class ChannelRelaysModel: ObservableObject` **Singleton**: `ChannelRelaysModel.shared` -**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L336) +**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L354) Holds runtime relay state for the currently viewed channel. Used by `ChannelRelaysView` to display and manage relays. Reset when the view is dismissed. @@ -283,14 +283,14 @@ Holds runtime relay state for the currently viewed channel. Used by `ChannelRela | Property | Type | Description | Line | |----------|------|-------------|------| -| `groupId` | `Int64?` | Group ID of the channel whose relays are loaded | [L338](../Shared/Model/ChatModel.swift#L338) | -| `groupRelays` | `[GroupRelay]` | Current relay instances for the channel | [L339](../Shared/Model/ChatModel.swift#L339) | +| `groupId` | `Int64?` | Group ID of the channel whose relays are loaded | [L356](../Shared/Model/ChatModel.swift#L356) | +| `groupRelays` | `[GroupRelay]` | Current relay instances for the channel | [L357](../Shared/Model/ChatModel.swift#L357) | ### Methods | Method | Description | Line | |--------|-------------|------| -| `set(groupId:groupRelays:)` | Populate all properties at once | [L341](../Shared/Model/ChatModel.swift#L341) | +| `set(groupId:groupRelays:)` | Populate all properties at once | [L359](../Shared/Model/ChatModel.swift#L359) | | `reset()` | Clear all properties to nil/empty | [L346](../Shared/Model/ChatModel.swift#L346) | --- @@ -468,6 +468,7 @@ App-level UI settings stored in `UserDefaults.standard`: | `DEFAULT_SHOW_LA_NOTICE` | `Bool` | Show LA setup notice | | `DEFAULT_NOTIFICATION_ALERT_SHOWN` | `Bool` | Notification permission alert shown | | `DEFAULT_CALL_KIT_CALLS_IN_RECENTS` | `Bool` | Show CallKit calls in recents | +| `DEFAULT_DIRECTORY_SEARCH_ALERT_SHOWN` | `Bool` | The user chose "Search and don't show again" in the online search alert; in `hintDefaults` (see [Chat List, Online Search](client/chat-list.md#11-online-search)) | ### GroupDefaults