mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 17:31:55 +00:00
ios: hide a search result once the chat opened from it is listed
A result's local chat was looked up only when the search ran, so a group or channel opened from its row stayed listed twice, as the chat and as the row, until the next search (a test run joined cinews_digest from the Directory part and saw both). The rows without a chat look it up again when a connection started from one ends.
This commit is contained in:
1 parent
7165bb7d02
commit
1c24109730
4 files changed
+45
-16
No files matched your search
@@ -129,7 +129,7 @@ struct NameSearchResult: Identifiable {
|
||||
let descr: String?
|
||||
let image: String?
|
||||
let iconName: String
|
||||
let localChatId: ChatId?
|
||||
var localChatId: ChatId?
|
||||
// the link the core resolved the name to, never a profile field the name's owner wrote
|
||||
let resolvedLink: String?
|
||||
|
||||
@@ -172,7 +172,7 @@ struct NameSearchResult: Identifiable {
|
||||
|
||||
struct DirectoryResult: Identifiable {
|
||||
let entry: DirectorySearchEntry
|
||||
let localChatId: ChatId?
|
||||
var localChatId: ChatId?
|
||||
|
||||
var id: String { entry.id }
|
||||
}
|
||||
|
||||
@@ -489,7 +489,7 @@ struct ChatListView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
.onTapGesture {
|
||||
searchFocussed = false
|
||||
planAndConnect(result.target, theme: theme, dismiss: false, cleanup: nil)
|
||||
planAndConnect(result.target, theme: theme, dismiss: false, cleanup: { Task { await onlineSearch.refreshLocalChatIds() } })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ struct ChatListView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
.onTapGesture {
|
||||
searchFocussed = false
|
||||
planAndConnect(result.entry.link, theme: theme, dismiss: false, cleanup: nil)
|
||||
planAndConnect(result.entry.link, theme: theme, dismiss: false, cleanup: { Task { await onlineSearch.refreshLocalChatIds() } })
|
||||
}
|
||||
}
|
||||
switch rows.footer {
|
||||
|
||||
@@ -114,6 +114,35 @@ class OnlineSearchModel: ObservableObject {
|
||||
)
|
||||
}
|
||||
|
||||
// a chat opened from a result exists only once its connection flow ends, so rows without a chat look again then
|
||||
func refreshLocalChatIds() async {
|
||||
let gen = generation
|
||||
var nameChatIds: [String: ChatId] = [:]
|
||||
for result in names where result.localChatId == nil {
|
||||
nameChatIds[result.id] = await localChatId(result.target)
|
||||
}
|
||||
var directoryChatIds: [String: ChatId] = [:]
|
||||
for result in directory where result.localChatId == nil {
|
||||
directoryChatIds[result.id] = await localChatId(result.entry.link)
|
||||
}
|
||||
guard gen == generation else { return }
|
||||
names = names.map { result in
|
||||
var r = result
|
||||
r.localChatId = r.localChatId ?? nameChatIds[r.id]
|
||||
return r
|
||||
}
|
||||
directory = directory.map { result in
|
||||
var r = result
|
||||
r.localChatId = r.localChatId ?? directoryChatIds[r.id]
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
private func localChatId(_ link: String) async -> ChatId? {
|
||||
let plan = await apiConnectPlan(connLink: link, resolveMode: .never, inProgress: BoxedValue(false), onFailure: .log)
|
||||
return plan.flatMap { planLocalChatId($0.connectionPlan) }
|
||||
}
|
||||
|
||||
private func resolveNames(_ name: String, generation gen: Int) async {
|
||||
let targets = name.hasPrefix("@") || name.hasPrefix("#") ? [name] : ["@" + name, "#" + name]
|
||||
var results: [NameSearchResult] = []
|
||||
@@ -140,8 +169,7 @@ class OnlineSearchModel: ObservableObject {
|
||||
var seen = Set(directory.map { $0.id })
|
||||
for entry in r.entries {
|
||||
guard seen.insert(entry.id).inserted else { continue }
|
||||
let plan = await apiConnectPlan(connLink: entry.link, resolveMode: .never, inProgress: BoxedValue(false), onFailure: .log)
|
||||
page.append(DirectoryResult(entry: entry, localChatId: plan.flatMap { planLocalChatId($0.connectionPlan) }))
|
||||
page.append(DirectoryResult(entry: entry, localChatId: await localChatId(entry.link)))
|
||||
}
|
||||
}
|
||||
guard gen == generation else { return }
|
||||
|
||||
@@ -331,9 +331,9 @@ The search text can be sent online on request: to SimpleX Directory, to find pub
|
||||
| Trigger | Handler | Runs |
|
||||
|---------|---------|------|
|
||||
| Keyboard Search key (`.submitLabel(.search)`) | [`runOnlineSearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1057) | `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#L1051) | `searchDirectory(text)`: the directory only |
|
||||
| "Search in Directory" row ([`SearchInDirectoryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L202)) | [`runDirectorySearch()`](../../Shared/Views/ChatList/ChatListView.swift#L1051) | `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.
|
||||
Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link, and both go through [`confirmDirectorySearch()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L333), 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#L341)): 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:
|
||||
|
||||
@@ -345,7 +345,7 @@ The rows above or below the search field follow the toolbar mode:
|
||||
| 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)
|
||||
### [`OnlineSearchModel`](../../Shared/Views/ChatList/DirectorySearchView.swift#L18-L200)
|
||||
|
||||
`@MainActor` `ObservableObject`, owned by `ChatListView` as `@StateObject onlineSearch` and passed to `ChatListSearchBar`.
|
||||
|
||||
@@ -375,12 +375,13 @@ The rows above or below the search field follow the toolbar mode:
|
||||
| [`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) |
|
||||
| [`refreshLocalChatIds()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L118) | The `cleanup` of a connection started from a result row, run when its flow ends: looks the local chat up again, store-only, for every row that has none (a name row by its target, a directory row by its link), unless the generation changed. A chat opened from a result is created during that flow, so its row is then hidden like any shown chat |
|
||||
|
||||
**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.
|
||||
**Spinner.** `loading` and `resolvingNames` have a `didSet` that calls [`updateSpinner()`](../../Shared/Views/ChatList/DirectorySearchView.swift#L186), 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.
|
||||
[`request(append:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L162) 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.
|
||||
|
||||
@@ -388,7 +389,7 @@ For each new entry of a page (not already shown, not repeated in the page), the
|
||||
|
||||
### 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.
|
||||
[`resolveNames(_:generation:)`](../../Shared/Views/ChatList/DirectorySearchView.swift#L146) 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
|
||||
|
||||
@@ -413,10 +414,10 @@ Each part renders only when it has rows: the Names part when `rows.names` is not
|
||||
|
||||
| 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#L695): "Search failed, tap to retry" | `retry()` |
|
||||
| Show more | [`DirectoryShowMoreRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L276) via [`directoryShowMoreRow()`](../../Shared/Views/ChatList/ChatListView.swift#L689): "Show more", disabled and in the secondary colour while `loading` | `loadMore()` |
|
||||
| Name | [`NameSearchRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L277): avatar, profile name in bold, the SimpleX name in parentheses (secondary), description (2 lines) | `planAndConnect(result.target, dismiss: false)`, `cleanup`: `refreshLocalChatIds()` |
|
||||
| Directory entry | [`DirectorySearchRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L224): 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)`, `cleanup`: `refreshLocalChatIds()` |
|
||||
| Retry | [`DirectoryRetryRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L319) via [`directoryRetryRow()`](../../Shared/Views/ChatList/ChatListView.swift#L695): "Search failed, tap to retry" | `retry()` |
|
||||
| Show more | [`DirectoryShowMoreRow`](../../Shared/Views/ChatList/DirectorySearchView.swift#L304) via [`directoryShowMoreRow()`](../../Shared/Views/ChatList/ChatListView.swift#L689): "Show more", disabled and in the secondary colour 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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user