From 2aa7ff31043eb5ebe0f820e4351aa388565773d8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:19:05 +0400 Subject: [PATCH] wip --- plans/2026-08-04-directory-search-in-app.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plans/2026-08-04-directory-search-in-app.md b/plans/2026-08-04-directory-search-in-app.md index c96990bf1d..081d204426 100644 --- a/plans/2026-08-04-directory-search-in-app.md +++ b/plans/2026-08-04-directory-search-in-app.md @@ -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 \" 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 \" 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