This commit is contained in:
spaced4ndy
2026-08-05 19:19:05 +04:00
parent b9cc71d531
commit 2aa7ff3104
+10 -8
View File
@@ -55,7 +55,7 @@ Results are not chats and are never written to the DB or to disk — they are vi
### Interpretations I made (say if any is wrong)
- **"Search both by name and in the directory"** = the Search key keeps the local filter, additionally resolves the typed name **online** when it is a name candidate not already known locally, and runs the directory search. The "Search in Directory" row does the directory part only. One warning covers both triggers.
- **Ordering**: local chats, then a "Directory" section header, then directory results, then "Show more". Name results sit with the local chats (§5).
- **Ordering**: three sections — local chats (unheadered, as today), then a "Names" section, then a "Directory" section ending in "Show more". Each new section, header included, is rendered only when it has results.
- **Scope**: both surfaces, as asked.
- **A result row's SimpleX name is de-emphasised, and the tap connects by name when there is one.** Everywhere else a name carries its verification state — checkmark when verified, red cross when not, a "Verify name" action when unknown (SimplexNameView.kt:76-97; ChatInfoView.swift:1442-1470) — and bare text is used only where re-verification is impossible. A result row cannot use that component (§5), and a hostile directory could attach any name to any link. Connecting by *link* does not check it: a `CTLink` target yields no plan name (Commands.hs:4445-4446) and so no `planDomain` (:4482), leaving the domain check at :4491-4498 dormant. So the row renders the name in secondary colour with no badge and no verify affordance, and the tap targets `simplexName` when present (§5) so that check does fire and a mismatch fails the connect. The cost is not only the extra resolution per tap: it also decides joinability when name resolution is unavailable, so §5 partitions the failure classes. If that is more machinery than the check is worth, drop `simplexName` from the row entirely and tap the link — but drop it, rather than showing a name the tap does not verify.
@@ -145,14 +145,16 @@ The requester side needs no flag — `processServiceRequests` gates only inbound
Two new row types, neither a `Chat`. Both lists already take heterogeneous items, so the change is additive.
**Placement.** Declaration order is the same in both bar positions — search bar, local matches, then the Directory section — because the search bar is declared first on both platforms regardless of mode (Kotlin `stickyHeader` ChatListView.kt:952, before `itemsIndexed(chats…)` :1001; iOS search-bar row ChatListView.swift:399-412, before the chats `ForEach` :422/:431). One-hand mode flips only the physical direction (`reverseLayout = oneHandUI.value`, ChatListView.kt:949, default true at SimpleXAPI.kt:269; a list-level `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` at ChatListView.swift:232, default true at :174), so directory results sit past the local matches either way. That is fine because the list is filtered during a search and therefore short — but check both modes against a term that matches many chats.
**Placement.** Declaration order is the same in both bar positions — search bar, local matches, the Names section, then the Directory section — because the search bar is declared first on both platforms regardless of mode (Kotlin `stickyHeader` ChatListView.kt:952, before `itemsIndexed(chats…)` :1001; iOS search-bar row ChatListView.swift:399-412, before the chats `ForEach` :422/:431). One-hand mode flips only the physical direction (`reverseLayout = oneHandUI.value`, ChatListView.kt:949, default true at SimpleXAPI.kt:269; a list-level `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` at ChatListView.swift:232, default true at :174), so directory results sit past the local matches either way. That is fine because the list is filtered during a search and therefore short — but check both modes against a term that matches many chats.
- **Kotlin** — header item, `items(results, key = …)` and the "Show more" item, after `itemsIndexed(chats, …)` (ChatListView.kt:1001) and **before** `ChatListFeatureCards` (:1007-1009), so the promo card stays at the end of the list; `ToggleChatListCard` (:996-1000) sits above the chats and is unaffected.
Each section is a header item plus its rows, emitted only when non-empty, so a search that matches nothing locally shows just the Directory header and its results — and a search with no directory hits shows no Directory header at all.
- **Kotlin** — the two header items, `items(names, …)`, `items(results, key = …)` and the "Show more" item, after `itemsIndexed(chats, …)` (ChatListView.kt:1001) and **before** `ChatListFeatureCards` (:1007-1009), so the promo card stays at the end of the list; `ToggleChatListCard` (:996-1000) sits above the chats and is unaffected.
- **iOS** — the same sections relative to the chats `ForEach` (ChatListView.swift:422/431), each row carrying the per-row `.scaleEffect` flip the other rows use.
**Directory row.** Display name with its `#`/`@` prefix, the SimpleX name de-emphasised when present (see the interpretation above), `shortDescr`, member count from `DETGroup.summary`, placeholder avatar (D2). Do not reuse `SimplexNameView`: it takes a `verify` closure and auto-verifies under `DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES` (ChatInfoView.swift:1411-1419), so a ten-row list could fire ten network verifications — and the name is the directory's claim (it emits one only for a domain it has verified, Listing.hs:116), not ours to badge. The directory lists both channels and groups, so the row picks its verb from `DETGroup.groupType` using the group/channel pair — `compose_view_join_channel` / `compose_view_join_group` (strings.xml:625-626), `"Join channel"` / `"Join group"` on iOS (ComposeView.swift:478) — defaulting to the group wording when `groupType` is absent. Not `connect_plan_join_name` / `connect_plan_connect_to_name`, which is a contact-vs-channel choice (ConnectPlan.kt:87).
**Name row.** When the Search key resolves the typed name online to a contact or channel that is not already a local chat, it renders as the same kind of connectable row, fed from the `apiConnectPlan` result (its `connLink` plus the short-link profile data) rather than from a `DirectoryEntry`: profile name, short description, contact-vs-channel verb, no member count. It sits with the local chats, above the Directory section. A name that resolves to nothing produces no row and — per §4 — no alert. Once the row exists it supersedes the "Connect to \<name\>" action row for that input, which is on screen for exactly the same text: hide the action row, since the result row carries the resolved profile and the action row only carries the guess.
**Name row.** When the Search key resolves the typed name online to a contact or channel that is not already a local chat, it renders as the same kind of connectable row, fed from the `apiConnectPlan` result (its `connLink` plus the short-link profile data) rather than from a `DirectoryEntry`: profile name, short description, contact-vs-channel verb, no member count. These form their own section between the local chats and the Directory section, bounded at **two** rows: a bare term is resolved as both `@name` and `#name`, a prefixed one only as itself (ChatListView.kt:819; ChatListView.swift:741), so there is nothing to paginate. A name that resolves to nothing produces no row and — per §4 — no alert; if neither variant resolves, the section is absent. Once the row exists it supersedes the "Connect to \<name\>" action row for that input, which is on screen for exactly the same text: hide the action row, since the result row carries the resolved profile and the action row only carries the guess.
**Links and keys.** `PublicLink` has **both** fields optional (Listing.hs:63-66): a public group yields short-only (:127-128), a group registered by group link may yield full-only (:130-132). Tapping accepts any of the three forms (ConnectPlan.kt:24-26), and the directory drops entries with no link. Preferring the name is what arms the domain check (see the interpretation above), but it cannot be an unconditional preference: a name target resolves **before** any link data is fetched, and `APIConnectPlan` does not degrade a resolution failure into a `CPError` plan (Commands.hs:2151-2152 — contrast `Connect` at :2378, which wraps it in `catchAllErrors`), so the failure reaches `apiConnectResponseAlert` (SimpleXAPI.kt:1540; unconditional on iOS, SimpleXAPI.swift:1046) and the entry cannot be joined at all — with its perfectly good link sitting unused in the row. Partition the rule by error class; a blanket retry-with-the-link would hand a hostile directory the attack back, since it could pair name X with link Y and pick an X that fails to resolve:
@@ -170,9 +172,9 @@ Two gates affect reachability, differently per platform: on iOS the search bar r
## 6. Pagination
The view model holds `entries`, `cursor`, `loading`, `error` — no result count, since nothing in the UI renders one and `cursor = Nothing` already says when the list is complete. "Show more" is the last row of the directory section: it re-calls with the stored cursor, appends (de-duplicating by key), and disappears when the response has `cursor = Nothing`. On failure it becomes a retry row carrying the message. An explicit button rather than infinite scroll also preserves the "requests only on an explicit action" property the warning promises.
Pagination is **directory-only**: the Names section is bounded at two rows by construction (§5), so it has no cursor and no "Show more". The directory view model holds `entries`, `cursor`, `loading`, `error` — no result count, since nothing in the UI renders one and `cursor = Nothing` already says when the list is complete. "Show more" is the last row of the Directory section: it re-calls with the stored cursor, appends (de-duplicating by key), and disappears when the response has `cursor = Nothing`. On failure it becomes a retry row carrying the message. An explicit button rather than infinite scroll also preserves the "requests only on an explicit action" property the warning promises.
Changing the search text discards `entries` and `cursor`; so does switching profile or remote host. `activeChatTagFilter` already clears `searchText` (ChatListView.kt:1034-1036) and must clear results with it.
Changing the search text discards both sections' results and the cursor; so does switching profile or remote host. `activeChatTagFilter` already clears `searchText` (ChatListView.kt:1034-1036) and must clear results with it.
## 7. Warning and preference
@@ -186,7 +188,7 @@ If two recipients in one warning is too much, the alternative is to drop online
## 8. Strings
Kotlin: new entries in `common/src/commonMain/resources/MR/base/strings.xml`, beside `connect_plan_connect_to_name` (:19) and `search_or_paste_simplex_link` (:519). iOS: inline `NSLocalizedString` at the use site, as `ConnectByNameRow` does (ChatListView.swift:815). Needed: the row label, section header, "Show more", the alert title/body/buttons, the empty state, and the error text.
Kotlin: new entries in `common/src/commonMain/resources/MR/base/strings.xml`, beside `connect_plan_connect_to_name` (:19) and `search_or_paste_simplex_link` (:519). iOS: inline `NSLocalizedString` at the use site, as `ConnectByNameRow` does (ChatListView.swift:815). Needed: the row label, the two section headers, "Show more", the alert title/body/buttons, the empty state, and the error text.
## 9. Failure modes to handle
@@ -201,7 +203,7 @@ Kotlin: new entries in `common/src/commonMain/resources/MR/base/strings.xml`, be
| Directory over its global cap | indistinguishable from a timeout to the client (§1) — same retry row |
| Tap: user has no names-role server | rows render without the name; the tap uses the link (§5) |
| Tap: the directory's name does not resolve, or mismatches the link | join refused with a mismatch message — never falls back to the link (§5) |
| No directory results | dedicated empty state |
| No directory results | Directory section omitted entirely; the empty state appears only when every section is empty |
| Result is a channel already joined | `planAndConnect` opens it (`GroupLinkPlan.Known`, ConnectPlan.kt:322-333) — no pre-check needed |
## 10. Order of work