mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 13:11:55 +00:00
android, desktop: show directory results you already have as chats, and say when a page adds none
As on iOS: a directory result whose link resolves to a chat the screen doesn't list is drawn with that screen's chat row, which opens it, instead of "Join group" / "Join channel": ChatListNavLinkView in the chat list, ContactListNavLinkView (now with a group branch) in New chat. When every result of the last page is a chat already shown, the Directory part says "All results on this page are already in your chats" above "Show more". Run: 008-dirsearch-known-chats, slice 3
This commit is contained in:
1 parent
9313a490bc
commit
d24d788a64
13 files changed
+134
-66
No files matched your search
+2
-1
@@ -183,7 +183,8 @@ data class DirectoryResult(
|
||||
data class OnlineSearchRows(
|
||||
val names: List<NameSearchResult>,
|
||||
val directory: List<DirectoryResult>,
|
||||
val pageInChats: Boolean,
|
||||
val footer: DirectoryFooter?,
|
||||
) {
|
||||
val isEmpty: Boolean get() = names.isEmpty() && directory.isEmpty() && footer == null
|
||||
val isEmpty: Boolean get() = names.isEmpty() && directory.isEmpty() && !pageInChats && footer == null
|
||||
}
|
||||
+17
-9
@@ -532,17 +532,25 @@ private fun LazyListScope.nameRows(names: List<NameSearchResult>, onlineSearch:
|
||||
}
|
||||
|
||||
private fun LazyListScope.directoryRows(rows: OnlineSearchRows, onlineSearch: OnlineSearchModel, oneHandUI: Boolean) {
|
||||
if (rows.directory.isNotEmpty() || rows.footer != null) {
|
||||
if (rows.directory.isNotEmpty() || rows.pageInChats || rows.footer != null) {
|
||||
searchPart(MR.strings.online_search_directory, oneHandUI) {
|
||||
items(rows.directory, key = { it.id }) { result ->
|
||||
val view = LocalMultiplatformView()
|
||||
DirectorySearchRow(
|
||||
result.entry,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.entry.link, close = null, cleanup = { withApi { onlineSearch.refreshLocalChatIds() } }) }
|
||||
}
|
||||
)
|
||||
val chat = result.localChatId?.let { chatModel.getChat(it) }
|
||||
if (chat != null) {
|
||||
ChatListNavLinkView(chat, remember { mutableStateOf(false) })
|
||||
} else {
|
||||
val view = LocalMultiplatformView()
|
||||
DirectorySearchRow(
|
||||
result.entry,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.entry.link, close = null, cleanup = { withApi { onlineSearch.refreshLocalChatIds() } }) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (rows.pageInChats) {
|
||||
item { DirectoryPageInChatsRow() }
|
||||
}
|
||||
when (rows.footer) {
|
||||
DirectoryFooter.Retry -> item { DirectoryRetryRow(onlineSearch) }
|
||||
|
||||
+20
@@ -49,6 +49,7 @@ class OnlineSearchModel {
|
||||
private set
|
||||
|
||||
private var cursor by mutableStateOf<JsonObject?>(null)
|
||||
private var lastPageIds by mutableStateOf<Set<String>>(emptySet())
|
||||
private var searchedText = ""
|
||||
// bumped on every reset, so a reply that arrives too late cannot refill a list already cleared
|
||||
private var generation = 0
|
||||
@@ -63,6 +64,7 @@ class OnlineSearchModel {
|
||||
names = emptyList()
|
||||
directory = emptyList()
|
||||
cursor = null
|
||||
lastPageIds = emptySet()
|
||||
loading = false
|
||||
resolvingNames = false
|
||||
failed = false
|
||||
@@ -102,6 +104,7 @@ class OnlineSearchModel {
|
||||
if (text == searchedText) {
|
||||
directory = emptyList()
|
||||
cursor = null
|
||||
lastPageIds = emptySet()
|
||||
failed = false
|
||||
searched = false
|
||||
} else {
|
||||
@@ -130,11 +133,16 @@ class OnlineSearchModel {
|
||||
val nameLinks = names.mapNotNull { it.resolvedLink }.toSet()
|
||||
fun isAmong(ids: Set<ChatId>, id: ChatId?): Boolean =
|
||||
id?.let { ids.contains(it) } ?: false
|
||||
val lastPage = directory.filter { lastPageIds.contains(it.id) }
|
||||
return OnlineSearchRows(
|
||||
names = names.filter { !isAmong(shownChatIds, it.localChatId) },
|
||||
directory = directory.filter {
|
||||
!isAmong(shownChatIds, it.localChatId) && !isAmong(nameChatIds, it.localChatId) && !nameLinks.contains(it.entry.link)
|
||||
},
|
||||
// a result hidden only because a name resolved to its link is on screen, but not one of your chats
|
||||
pageInChats = lastPage.isNotEmpty() && lastPage.all {
|
||||
isAmong(shownChatIds, it.localChatId) || isAmong(nameChatIds, it.localChatId)
|
||||
},
|
||||
footer = footer
|
||||
)
|
||||
}
|
||||
@@ -202,6 +210,7 @@ class OnlineSearchModel {
|
||||
}
|
||||
cursor = r.cursor
|
||||
directory = if (append) directory + page else page
|
||||
lastPageIds = page.map { it.id }.toSet()
|
||||
}
|
||||
|
||||
private fun updateSpinner() {
|
||||
@@ -315,6 +324,17 @@ fun NameSearchRow(result: NameSearchResult, modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DirectoryPageInChatsRow() {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
Text(stringResource(MR.strings.directory_search_page_in_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DirectoryShowMoreRow(onlineSearch: OnlineSearchModel) {
|
||||
Row(
|
||||
|
||||
+20
@@ -135,6 +135,26 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
|
||||
selectedChat,
|
||||
nextChatSelected)
|
||||
}
|
||||
is ChatInfo.Group -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled, showDeletedChatIcon)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
openChat(secondaryChatsCtx = null, rhId, chat.chatInfo)
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
},
|
||||
dropdownMenuItems = null,
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ fun ContactPreviewView(
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
is ChatInfo.ContactRequest, is ChatInfo.Group ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NameWithBadge(
|
||||
cInfo.chatViewName,
|
||||
|
||||
+17
-9
@@ -209,18 +209,26 @@ private fun ModalData.NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rows.directory.isNotEmpty() || rows.footer != null) {
|
||||
if (rows.directory.isNotEmpty() || rows.pageInChats || rows.footer != null) {
|
||||
item { SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) }
|
||||
searchPart(MR.strings.online_search_directory, oneHandUI.value, header = { OnlineSearchHeader(it) }) {
|
||||
items(rows.directory, key = { it.id }) { result ->
|
||||
val view = LocalMultiplatformView()
|
||||
DirectorySearchRow(
|
||||
result.entry,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.entry.link, close = close, cleanup = null) }
|
||||
}
|
||||
)
|
||||
val chat = result.localChatId?.let { chatModel.getChat(it) }
|
||||
if (chat != null) {
|
||||
ContactListNavLinkView(chat, remember { mutableStateOf(false) }, showDeletedChatIcon = true)
|
||||
} else {
|
||||
val view = LocalMultiplatformView()
|
||||
DirectorySearchRow(
|
||||
result.entry,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.entry.link, close = close, cleanup = null) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (rows.pageInChats) {
|
||||
item { DirectoryPageInChatsRow() }
|
||||
}
|
||||
when (rows.footer) {
|
||||
DirectoryFooter.Retry -> item { DirectoryRetryRow(onlineSearch) }
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="directory_search_n_members">%1$d members</string>
|
||||
<string name="directory_search_show_more">Show more</string>
|
||||
<string name="directory_search_failed_retry">Search failed, tap to retry</string>
|
||||
<string name="directory_search_page_in_chats">All results on this page are already in your chats</string>
|
||||
<string name="searching_directory">Searching directory…</string>
|
||||
<string name="online_search_names">Names</string>
|
||||
<string name="online_search_directory">Directory</string>
|
||||
|
||||
@@ -14,7 +14,7 @@ This document catalogs known gaps in the multiplatform codebase (Android and Des
|
||||
6. [Desktop: Recording Not Implemented](#gap-06-desktop-recording-not-implemented)
|
||||
7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented)
|
||||
8. [Desktop: Emoji Picker Without Skin Tones and Localized Search](#gap-08-desktop-emoji-picker-without-skin-tones-and-localized-search)
|
||||
9. [Online Search: Groups Joined Through Another Link Still Appear as Directory Results](#gap-09-online-search-groups-joined-through-another-link-still-appear-as-directory-results)
|
||||
9. [Online Search: Groups Joined Through Another Link Are Not Recognised](#gap-09-online-search-groups-joined-through-another-link-are-not-recognised)
|
||||
10. [Online Search: First-Search Alert Does Not Mention the Server Hosting a Resolved Address](#gap-10-online-search-first-search-alert-does-not-mention-the-server-hosting-a-resolved-address)
|
||||
11. [Online Search: New Chat Does Not Narrow Contacts by Name While Typing](#gap-11-online-search-new-chat-does-not-narrow-contacts-by-name-while-typing)
|
||||
|
||||
@@ -322,7 +322,7 @@ Desktop users cannot pick a skin tone in the picker, and search in other languag
|
||||
|
||||
---
|
||||
|
||||
## GAP-09: Online Search: Groups Joined Through Another Link Still Appear as Directory Results
|
||||
## GAP-09: Online Search: Groups Joined Through Another Link Are Not Recognised
|
||||
|
||||
**Severity:** Low
|
||||
**Category:** UI / UX
|
||||
@@ -330,7 +330,9 @@ Desktop users cannot pick a skin tone in the picker, and search in other languag
|
||||
|
||||
### Description
|
||||
|
||||
A directory entry is matched to a local chat by a store-only lookup of its link (`apiConnectPlan` with `PlanResolveMode.PRMNever`, then `planLocalChatId`). A regular group's entry carries the directory's own group link (short link only), so a group the user joined through a different link, through the directory's full link, or by invitation is not found and is shown again in the Directory part. Tapping it goes through `planAndConnect`, which handles the known group.
|
||||
A directory entry is matched to a local chat by a store-only lookup of its link (`apiConnectPlan` with `PlanResolveMode.PRMNever`, then `planLocalChatId`). An entry the app finds this way opens as a chat: the screen's chat row is drawn in place of the entry. 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 still shows as a directory entry with "Join group". Tapping it goes through `planAndConnect`, which handles the known group. Accepted.
|
||||
|
||||
Related: the note "All results on this page are already in your chats" is not shown when a page's only results are hidden because a name search resolved to the same link (that row is on screen, but not one of the user's chats); the Directory part then shows only "Show more" when there are more pages, and is not shown otherwise.
|
||||
|
||||
### Affected Locations
|
||||
|
||||
|
||||
@@ -300,9 +300,9 @@ This document specifies invariants enforced by the Android and Desktop (Kotlin/C
|
||||
|
||||
### RULE-22: Nothing Is Shown Twice
|
||||
|
||||
**Invariant:** A name result whose chat is shown in the local list MUST be hidden. A directory result MUST be hidden when its link refers, in the local store, to a chat shown in the local list or found by name, or when its link is the link a name was resolved through. Only store-derived chat ids and links resolved by the core may hide a row. Limit: a regular group joined through another link (or the directory's full link) is not found by the lookup and still appears as a directory result (GAP-09).
|
||||
**Invariant:** A name result whose chat is shown in the local list MUST be hidden. A directory result MUST be hidden when its link refers, in the local store, to a chat shown in the local list or found by name, or when its link is the link a name was resolved through. Only store-derived chat ids and links resolved by the core may hide a row. A directory result whose link refers to a chat the screen doesn't list MUST be drawn as that screen's chat row, which opens the chat, never as "Join …". When every result of the last page loaded is hidden as a chat the user has, the Directory part MUST say so ("All results on this page are already in your chats"); a result hidden only because a name was resolved through its link does not count. Limit: a regular group joined through another link, the directory's full link or by invitation is not found by the lookup and still appears as a directory result with "Join group" (GAP-09).
|
||||
|
||||
**Enforcement:** `OnlineSearchModel.rows(shownChatIds)` filters at render time with the ids of the chats each screen shows; `planLocalChatId` derives a result's local chat id from the connection plan, and `NameSearchResult.resolvedLink` is the link the core resolved.
|
||||
**Enforcement:** `OnlineSearchModel.rows(shownChatIds)` filters at render time with the ids of the chats each screen shows and computes `pageInChats`; `planLocalChatId` derives a result's local chat id from the connection plan, and `NameSearchResult.resolvedLink` is the link the core resolved. `directoryRows` (chat list) and `onlineSearchSections` (New chat) draw a result whose chat is loaded with `ChatListNavLinkView` / `ContactListNavLinkView`.
|
||||
|
||||
**Location:**
|
||||
- `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt` -- `OnlineSearchModel.rows()`
|
||||
|
||||
@@ -64,9 +64,11 @@ Searching beyond the user's own chats happens only when the user asks, and the r
|
||||
| "Connect to …" row | Shown for a SimpleX name, as before (on Android with the top bar it takes the tags' place); hidden once the online search found anything for that name |
|
||||
| First-search alert | Before the first online search: "Search in Directory?" — the text goes to SimpleX Directory, and a SimpleX name also to the user's server that resolves names; no contact is created and the profile is not sent. Search / Search and don't show again / Cancel ("Reset all hints" brings it back) |
|
||||
| Names part | Below the local chats, headed "Names": avatar, profile name, the SimpleX name in parentheses, description. Tapping connects (or opens the chat if it is already known) |
|
||||
| Directory part | Below the Names part, headed "Directory": avatar, `#` name, the directory's SimpleX name, description, member or subscriber count, and "Join channel" / "Join group". Tapping connects; "Show more" loads the next page; a failed request shows "Search failed, tap to retry" |
|
||||
| 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". An entry for a group or channel the user already has, that the list above doesn't show (its name doesn't match the typed text), is shown as the regular chat row and opens the chat, never "Join …". When every result of the last page loaded is a chat already in the list or found by name, the part shows "All results on this page are already in your chats" (secondary colour), followed by "Show more" when there are more pages; nothing loads more pages automatically |
|
||||
|
||||
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.
|
||||
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, or by invitation, is not recognised: it still appears in the Directory part with "Join group". 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 the one-hand UI the whole list is reversed: going up from the search field at the bottom come the local chats, then Names, then Directory, each part's header above its rows. This order is intended, so the user's own chats stay nearest the field.
|
||||
|
||||
While an online search runs or has results, the list shows no banner, no one-hand UI card and no connect cards. Typing alone, which only filters the chats, keeps them; they come back when the results go (the text changes or is cleared) or when a search is cancelled before showing anything.
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ The search field of the New chat sheet filters "Your contacts" and offers the sa
|
||||
| "Connect to …" row | Below the field for a SimpleX name; hidden once the online search found anything for that name |
|
||||
| "Search in Directory" row | Below the field (after "Connect to …") for non-empty text that is not a SimpleX link; searches SimpleX Directory |
|
||||
| Keyboard Search key | On Android the keyboard's Search action, on desktop Enter. Searches the directory and, for a SimpleX name, resolves the name online |
|
||||
| Names and Directory sections | After the contacts, each with a section header in the style of "Your contacts". Tapping a result connects and closes the sheet, as other New chat actions do. A result for a contact already listed is not repeated. With the one-hand UI the list is reversed, and each header is still shown above its rows |
|
||||
| Names and Directory sections | After the contacts, each with a section header in the style of "Your contacts". Tapping a result connects and closes the sheet, as other New chat actions do. A result for a contact already listed is not repeated. A directory entry for a group or channel the user already has is shown as a contact-style chat row (avatar, name, favourite star, incognito mask) that opens the chat and closes the sheet, never "Join …". When every result of the last page loaded is a chat the user has, the Directory section shows "All results on this page are already in your chats", followed by "Show more" when there are more pages. With the one-hand UI the list is reversed, and each header is still shown above its rows |
|
||||
| Contacts empty state | "No filtered contacts" is not shown while the online search runs or shows results |
|
||||
| Online search running | Progress indicator in the search bar; closing the sheet cancels the search |
|
||||
| Directory search failed | "Search failed, tap to retry" row at the end of the Directory section |
|
||||
|
||||
@@ -227,7 +227,7 @@ On desktop, the currently selected chat (`chatModel.chatId.value == chat.id`) re
|
||||
|
||||
### TagsView
|
||||
|
||||
**Location:** [`ChatListView.kt#L1451`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1451)
|
||||
**Location:** [`ChatListView.kt#L1451`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1459)
|
||||
|
||||
Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable).
|
||||
|
||||
@@ -374,20 +374,20 @@ Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage
|
||||
|
||||
**Files:** `views/chatlist/DirectorySearchView.kt` (model and rows), `model/DirectorySearch.kt` (response and result types), `views/chatlist/ChatListView.kt` (triggers and rendering), `views/helpers/SearchTextField.kt` (Search key)
|
||||
|
||||
The search text can be sent online on request: to SimpleX Directory, to find public groups and channels, and, when the text is a SimpleX name, to the user's name-resolving server. Results are shown below the local chats in two parts, Names then Directory. They are never saved and live only as long as the search text and the active profile: [`ChatListSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L887-L991) calls `reset()` from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L932-L937)). The model is a plain `remember` in [`ChatListView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L246), so an Android rotation (which recreates the Activity) drops the results while the `rememberSaveable` search text stays. The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)).
|
||||
The search text can be sent online on request: to SimpleX Directory, to find public groups and channels, and, when the text is a SimpleX name, to the user's name-resolving server. Results are shown below the local chats in two parts, Names then Directory. They are never saved and live only as long as the search text and the active profile: [`ChatListSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L895-L999) calls `reset()` from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L940-L945)). The model is a plain `remember` in [`ChatListView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L246), so an Android rotation (which recreates the Activity) drops the results while the `rememberSaveable` search text stays. The New chat sheet uses the same model and rows (see [Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)).
|
||||
|
||||
### Triggers
|
||||
|
||||
| Trigger | Handler | Runs |
|
||||
|---------|---------|------|
|
||||
| Search key: the keyboard's Search action on Android, Enter (key up) on desktop, through [`SearchTextField`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L33-L43)`(onSearch)` | [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1016-L1021) | `search(text, name = connectNameCandidate.value)`: the directory's first page, and name resolution when the text is a name candidate |
|
||||
| "Search in Directory" row ([`SearchInDirectoryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L224-L239)) | [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1009-L1014) | `searchDirectory(text)`: the directory only |
|
||||
| Search key: the keyboard's Search action on Android, Enter (key up) on desktop, through [`SearchTextField`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L33-L43)`(onSearch)` | [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1024-L1029) | `search(text, name = connectNameCandidate.value)`: the directory's first page, and name resolution when the text is a name candidate |
|
||||
| "Search in Directory" row ([`SearchInDirectoryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L233-L248)) | [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1017-L1022) | `searchDirectory(text)`: the directory only |
|
||||
|
||||
`SearchTextField` handles the key per platform: on Android through `KeyboardActions(onSearch)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L116-L121)), on desktop through `onPreviewKeyEvent` for `Key.Enter` / `Key.NumPadEnter` on `KeyUp` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt#L95-L102)), so one key press runs the search once. Every caller keeps `ImeAction.Search`; a caller without `onSearch` gets no handler.
|
||||
|
||||
Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link ([`searchTrimmed`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1006-L1007), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L349-L355), which runs the search at once when `appPrefs.directorySearchAlertShown` is set and otherwise shows the three-button alert ([`showDirectorySearchAlert`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L357-L385), `showAlertDialogButtonsColumn` with `parseHtml = false` so the paragraph break shows): Search / Search and don't show again / Cancel. Its message names both recipients: the directory, and, for a SimpleX name, the server that resolves names. The preference is in `hintPreferences`, so "Reset all hints" brings the alert back. Each trigger launches the model method with `withApi`.
|
||||
Both triggers do nothing for empty (trimmed) text or a pasted SimpleX link ([`searchTrimmed`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1014-L1015), `searchShowingSimplexLink`), and both go through [`confirmDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L369-L375), which runs the search at once when `appPrefs.directorySearchAlertShown` is set and otherwise shows the three-button alert ([`showDirectorySearchAlert`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L377-L405), `showAlertDialogButtonsColumn` with `parseHtml = false` so the paragraph break shows): Search / Search and don't show again / Cancel. Its message names both recipients: the directory, and, for a SimpleX name, the server that resolves names. The preference is in `hintPreferences`, so "Reset all hints" brings the alert back. Each trigger launches the model method with `withApi`.
|
||||
|
||||
The rows next to the search field share one slot with the tags, laid out by [`TagsOrConnectByName`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1320-L1374) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1241-L1266):
|
||||
The rows next to the search field share one slot with the tags, laid out by [`TagsOrConnectByName`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1328-L1382) and [`ChatListSearchBarWithTags`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1249-L1274):
|
||||
|
||||
| Search text | Android, one-hand UI (slot above the field) | Android, top bar (slot below the field) | Desktop (either mode) |
|
||||
|-------------|-----------------------------------|------------------------------|------|
|
||||
@@ -403,7 +403,7 @@ On desktop the rows are passed to the caller's `desktopView`, which places them
|
||||
|
||||
### OnlineSearchModel
|
||||
|
||||
**Location:** [`OnlineSearchModel`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L32-L221)
|
||||
**Location:** [`OnlineSearchModel`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L32-L230)
|
||||
|
||||
A plain class holding Compose state, owned by `ChatListView` as `remember { OnlineSearchModel() }` and passed through `ChatListWithLoadingScreen` to `ChatList`, `ChatListSearchBarWithTags`, `ChatListSearchBar` and `TagsOrConnectByName`. Every method runs on the main thread: the triggers and rows launch them with `withApi` (`Dispatchers.Main`), and the spinner's `onCancel` hops back with `withApi { cancelRequest() }`; the API calls move the FFI work to `Dispatchers.IO` themselves. This keeps every read and write of `generation` on one thread.
|
||||
|
||||
@@ -416,32 +416,33 @@ A plain class holding Compose state, owned by `ChatListView` as `remember { Onli
|
||||
| `failed` | The last directory request failed |
|
||||
| `searched` | A directory request for the current text completed, successfully or not (tells "not searched" from "found nothing") |
|
||||
| `cursor` (private) | Opaque `JsonObject?` from the last page, echoed back for the next one; null when there are no more. Snapshot state, because `footer` reads it |
|
||||
| `lastPageIds` (private) | The ids of the results the last successful request added (none when every entry of the page was already shown); cleared with the directory state, kept by a failed request. Snapshot state, because `rows` reads it |
|
||||
| `generation` (private) | Bumped by `reset()` and `cancelRequest()`; a reply whose captured generation differs never writes model state |
|
||||
|
||||
The public state has private setters; `searchedText`, `generation` and `spinnerShown` are plain fields.
|
||||
|
||||
| Computed | Definition |
|
||||
|----------|------------|
|
||||
| [`searching`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L57) | `loading \|\| resolvingNames` |
|
||||
| [`showResults`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L58) | `searching \|\| searched \|\| names.isNotEmpty()` |
|
||||
| [`footer`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L59) | `DirectoryFooter.Retry` when `failed`, else `ShowMore` when a cursor exists, else null |
|
||||
| [`searching`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L58) | `loading \|\| resolvingNames` |
|
||||
| [`showResults`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L59) | `searching \|\| searched \|\| names.isNotEmpty()` |
|
||||
| [`footer`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L60) | `DirectoryFooter.Retry` when `failed`, else `ShowMore` when a cursor exists, else null |
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| [`reset()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L61-L71) | Clears all results and flags, bumps `generation` (text change, profile switch) |
|
||||
| [`cancelRequest()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L74-L78) | The spinner's `onCancel`: bumps `generation` and clears `loading` and `resolvingNames`; results already shown stay |
|
||||
| [`search(text, name)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L80-L97) | Search key: ignored for empty text or while `searching` for the same text; otherwise `reset()`, then the first directory page and, with a name, name resolution concurrently (`coroutineScope { async { resolveNames(name, gen) } }`, with the generation captured before the child coroutine starts) |
|
||||
| [`searchDirectory(text)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L99-L112) | The row: ignored while `loading` for the same text; for the same text it clears only the directory state (name results stay), for new text it resets; then the first page |
|
||||
| [`loadMore()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L114-L117) | "Show more": the next page, appended |
|
||||
| [`retry()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L120-L126) | The retry row: resumes the page when a cursor exists, else repeats the first page; no reset |
|
||||
| [`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L128-L140) | The rows to render, given the ids of the local chats shown (see De-duplication) |
|
||||
| [`refreshLocalChatIds()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L142-L157) | 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. Name rows are copied with `withLocalChatId`, as `NameSearchResult` keeps its constructor private |
|
||||
| [`reset()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L62-L73) | Clears all results and flags, bumps `generation` (text change, profile switch) |
|
||||
| [`cancelRequest()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L76-L80) | The spinner's `onCancel`: bumps `generation` and clears `loading` and `resolvingNames`; results already shown stay |
|
||||
| [`search(text, name)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L82-L99) | Search key: ignored for empty text or while `searching` for the same text; otherwise `reset()`, then the first directory page and, with a name, name resolution concurrently (`coroutineScope { async { resolveNames(name, gen) } }`, with the generation captured before the child coroutine starts) |
|
||||
| [`searchDirectory(text)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L101-L115) | The row: ignored while `loading` for the same text; for the same text it clears only the directory state (name results stay), for new text it resets; then the first page |
|
||||
| [`loadMore()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L117-L120) | "Show more": the next page, appended |
|
||||
| [`retry()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L123-L129) | The retry row: resumes the page when a cursor exists, else repeats the first page; no reset |
|
||||
| [`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L131-L148) | The rows to render, given the ids of the local chats shown (see De-duplication) |
|
||||
| [`refreshLocalChatIds()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L150-L165) | 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, or drawn as a chat row when the list doesn't show it. Name rows are copied with `withLocalChatId`, as `NameSearchResult` keeps its constructor private |
|
||||
|
||||
**Spinner.** The private setters of `loading` and `resolvingNames` call [`updateSpinner()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L207-L220) on every write, which starts the "Searching directory…" progress with owner `ConnectProgressOwner.DirectorySearch` when `searching` becomes true and stops it when it becomes false (`spinnerShown` keeps the last state, so repeated writes do nothing). The chat list shows it in the toolbar ([`ChatListToolbar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L660-L666)) once it has run for a second, not in the search field. The spinner is shared with connections: [`ConnectProgressOwner`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L50) records whose it is, [`startConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L60-L69) does not let a search take it from a running connection, and [`stopConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L72-L78) ignores a stop from another owner. Tapping a result calls [`planAndConnect`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt#L24-L55), whose `cancelConnectProgress()` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L81-L88)) cancels the search through its `onCancel` before the connection takes the spinner. Clearing the search text does the same through the typing path.
|
||||
**Spinner.** The private setters of `loading` and `resolvingNames` call [`updateSpinner()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L216-L229) on every write, which starts the "Searching directory…" progress with owner `ConnectProgressOwner.DirectorySearch` when `searching` becomes true and stops it when it becomes false (`spinnerShown` keeps the last state, so repeated writes do nothing). The chat list shows it in the toolbar ([`ChatListToolbar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L668-L674)) once it has run for a second, not in the search field. The spinner is shared with connections: [`ConnectProgressOwner`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L50) records whose it is, [`startConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L60-L69) does not let a search take it from a running connection, and [`stopConnectProgress`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L72-L78) ignores a stop from another owner. Tapping a result calls [`planAndConnect`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt#L24-L55), whose `cancelConnectProgress()` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L81-L88)) cancels the search through its `onCancel` before the connection takes the spinner. Clearing the search text does the same through the typing path.
|
||||
|
||||
### Directory requests
|
||||
|
||||
[`request(append)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L182-L205) reads `chatModel.remoteHostId()` and calls [`apiSearchDirectory`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1645-L1652), which sends `APISendServiceRequest` to [`DIRECTORY_SERVICE_LINK`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L16) with a [10-second timeout](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L19) and the request built by [`directorySearchRequest`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L71-L75) (`searchText`, and `searchCursor` for later pages). [`parseDirectorySearchResponse`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L79-L88) accepts only a `searchResults` response and decodes its entries one at a time inside `runCatching`, so an entry the app cannot decode is dropped and the rest of the page is kept.
|
||||
[`request(append)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L190-L214) reads `chatModel.remoteHostId()` and calls [`apiSearchDirectory`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1645-L1652), which sends `APISendServiceRequest` to [`DIRECTORY_SERVICE_LINK`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L16) with a [10-second timeout](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L19) and the request built by [`directorySearchRequest`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L71-L75) (`searchText`, and `searchCursor` for later pages). [`parseDirectorySearchResponse`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L79-L88) accepts only a `searchResults` response and decodes its entries one at a time inside `runCatching`, so an entry the app cannot decode is dropped and the rest of the page is kept.
|
||||
|
||||
[`DirectorySearchEntry`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L33-L58) keeps the wire field `groupLink` private; its [`link`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L47-L57) is a `@Transient` property whose initializer, run by the generated decoder, takes `groupLink.connShortLink ?: groupLink.connFullLink` and throws `SerializationException` unless [`simplexLinkType`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L60-L63) classifies it as `SimplexLinkType.group` or `SimplexLinkType.channel`. Being `@Transient`, `link` cannot be set by a `link` key in the JSON. `simplexLinkType` returns the link type only when [`strConnectTarget`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt#L834) finds exactly one SimpleX link whose text is the whole string. `link` is also the entry's `id`. The directory sends only the short link when an entry has one.
|
||||
|
||||
@@ -449,37 +450,42 @@ For each new entry of a page (not already shown, not repeated in the page), the
|
||||
|
||||
### Name resolution
|
||||
|
||||
[`resolveNames(name, gen)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L164-L180) resolves `[name]` for a candidate starting with `@` or `#`, else `["@$name", "#$name"]` (the candidate comes from [`nameSearchCandidate`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1283-L1294), as on the typing path). Each target is planned with `apiConnectPlan(rh, target, resolveMode = PRMUnknown, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)`; it stops before a target once the generation changed, so a cancelled or replaced search sends no further names. [`NameSearchResult(target, result)`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L112-L174) (a private constructor behind a companion `operator fun invoke` that returns null for no row) maps the plan with one exhaustive `when`: a contact address or group link gives a row (profile from the link data or the known chat), the user's own contact address, an invitation link and an error give none. `simplexName` is the plan's `planSimplexName?.shortStr`, `localChatId` comes from `planLocalChatId`, `iconName` is an `ImageResource`, and for a group link `resolvedLink` is the link the core resolved (`connLink.connShortLink`). Failures are logged at debug level and never shown as alerts; the false `inProgress` also keeps `sendCmdWithRetry` from showing its network retry alert.
|
||||
[`resolveNames(name, gen)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L172-L188) resolves `[name]` for a candidate starting with `@` or `#`, else `["@$name", "#$name"]` (the candidate comes from [`nameSearchCandidate`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1291-L1302), as on the typing path). Each target is planned with `apiConnectPlan(rh, target, resolveMode = PRMUnknown, inProgress = mutableStateOf(false), onFailure = ConnectPlanFailure.Log)`; it stops before a target once the generation changed, so a cancelled or replaced search sends no further names. [`NameSearchResult(target, result)`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L112-L174) (a private constructor behind a companion `operator fun invoke` that returns null for no row) maps the plan with one exhaustive `when`: a contact address or group link gives a row (profile from the link data or the known chat), the user's own contact address, an invitation link and an error give none. `simplexName` is the plan's `planSimplexName?.shortStr`, `localChatId` comes from `planLocalChatId`, `iconName` is an `ImageResource`, and for a group link `resolvedLink` is the link the core resolved (`connLink.connShortLink`). Failures are logged at debug level and never shown as alerts; the false `inProgress` also keeps `sendCmdWithRetry` from showing its network retry alert.
|
||||
|
||||
### De-duplication
|
||||
|
||||
[`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L128-L140) returns an [`OnlineSearchRows`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L183-L189) at composition time, so pages added by "Show more" are filtered too:
|
||||
[`rows(shownChatIds)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L131-L148) returns an [`OnlineSearchRows`](../../common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt#L183-L190) at composition time, so pages added by "Show more" are filtered too:
|
||||
|
||||
- a name result is hidden when its `localChatId` is among the shown local chats;
|
||||
- a directory result is hidden when its `localChatId` is among the shown local chats or any name result's `localChatId`, or when its `entry.link` equals any name result's `resolvedLink` (all name results, shown or not);
|
||||
- `pageInChats` is true when the last page's results (`lastPageIds`) are not empty and each one's `localChatId` is among the shown local chats or the name results' chat ids. A result hidden only because a name result resolved to its link is not a chat the user has, so it does not count, and the part keeps its usual look;
|
||||
- `footer` passes through.
|
||||
|
||||
Only store-derived chat ids and the link the core resolved can hide a row, never a field the directory or a name's owner wrote. A mismatch in link encoding can only produce a duplicate row.
|
||||
|
||||
[GAP] A regular group the user joined through a different link, or through the directory's full link, is not found by the store lookup and still appears as a Directory row; tapping it goes through `planAndConnect`. See [GAP-09](../../product/gaps.md#gap-09-online-search-groups-joined-through-another-link-still-appear-as-directory-results).
|
||||
A directory result that is a chat the user has but that the screen doesn't list (the local filter doesn't match it) stays in `rows.directory` and is drawn as a chat row (see Rendering). When its chat id is not among the loaded chats (`chatModel.getChat` returns null), the Directory row is drawn as before, and tapping it goes through `planAndConnect`, which opens the known chat.
|
||||
|
||||
[GAP] A regular group the user joined through a different link, through the directory's full link, or by invitation, is not found by the store lookup and still appears as a Directory row with "Join group"; tapping it goes through `planAndConnect`. See [GAP-09](../../product/gaps.md#gap-09-online-search-groups-joined-through-another-link-are-not-recognised).
|
||||
|
||||
### Rendering
|
||||
|
||||
| Where | Rows | Empty state |
|
||||
|-------|------|-------------|
|
||||
| [`ChatList`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1066-L1239) | `rows(shownChatIds)` with the ids of the shown chats ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1109)); after the local chats and before `ChatListFeatureCards`, [`nameRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L517-L532) then [`directoryRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L534-L554) | `NoChatsView` overlay only when no chat is shown, the model is not `searching` and `rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1217-L1222)); the `you_have_no_chats` overlay of `ChatListWithLoadingScreen` (no chats at all) is hidden while `showResults` |
|
||||
| [`ChatList`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1074-L1247) | `rows(shownChatIds)` with the ids of the shown chats ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1117)); after the local chats and before `ChatListFeatureCards`, [`nameRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L517-L532) then [`directoryRows`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L534-L562) | `NoChatsView` overlay only when no chat is shown, the model is not `searching` and `rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1225-L1230)); the `you_have_no_chats` overlay of `ChatListWithLoadingScreen` (no chats at all) is hidden while `showResults` |
|
||||
| Onboarding branch of [`ChatListWithLoadingScreen`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L459-L506) | A `Column` with `ChatListSearchBarWithTags` first (top bar) or last (one-hand UI), and between them a `Box(Modifier.weight(1f))` holding, while `showResults`, a `LazyColumnWithScrollBarNoAppBar` of the same parts with `shownChatIds = emptySet()` (reversed in one-hand mode, `imePadding()` in top-bar mode), otherwise `ConnectOnboardingView()` on Android | A centred "No chats found" item when not searching and `rows.isEmpty` |
|
||||
|
||||
The onboarding `Column` takes the toolbar and navigation-bar paddings and consumes the navigation-bar insets, so the results list's `imePadding()` does not count them twice. On desktop the onboarding pages are drawn in the centre pane, so the chat list pane shows only the search bar and the results. When a joined result adds a chat and the onboarding state ends, `ChatList` enters composition and its `LaunchedEffect(activeFilter.value)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1236-L1238)) clears the search text, which resets the model; the iOS app keeps the search there.
|
||||
The onboarding `Column` takes the toolbar and navigation-bar paddings and consumes the navigation-bar insets, so the results list's `imePadding()` does not count them twice. On desktop the onboarding pages are drawn in the centre pane, so the chat list pane shows only the search bar and the results. When a joined result adds a chat and the onboarding state ends, `ChatList` enters composition and its `LaunchedEffect(activeFilter.value)` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1244-L1246)) clears the search text, which resets the model; the iOS app keeps the search there.
|
||||
|
||||
Each part renders only when it has rows: the Names part when `rows.names` is not empty, the Directory part when `rows.directory` is not empty or `rows.footer` is set. `nameRows` and `directoryRows` are `LazyListScope` builders; each part has a header `item` ([`SearchPartHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L567-L575): `body2`, secondary colour, `semantics { heading() }`) rather than a `stickyHeader`, and its rows are `items` keyed by `id`. [`searchPart`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L556-L565) emits the header before the part's rows with the top bar and after them in the one-hand UI, so it is drawn above them in both layouts, the one-hand list being reversed. Its header defaults to `SearchPartHeader`; the New chat sheet passes its own ([Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)). A lazy list keeps its first visible item in place by key, so in the reversed onboarding list, whose first item is then a keyed row, names arriving after the directory page can end up out of view: that list scrolls to its first item when name results arrive in one-hand mode ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L474-L480)). The `ChatList` branch starts with a keyless spacer, which stays the anchor.
|
||||
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, `rows.pageInChats` or `rows.footer` is set. `nameRows` and `directoryRows` are `LazyListScope` builders; each part has a header `item` ([`SearchPartHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L575-L583): `body2`, secondary colour, `semantics { heading() }`) rather than a `stickyHeader`, and its rows are `items` keyed by `id`. [`searchPart`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L564-L573) emits the header before the part's rows with the top bar and after them in the one-hand UI, so it is drawn above them in both layouts, the one-hand list being reversed. Its header defaults to `SearchPartHeader`; the New chat sheet passes its own ([Navigation, New Chat Online Search](navigation.md#10-new-chat-online-search)). A lazy list keeps its first visible item in place by key, so in the reversed onboarding list, whose first item is then a keyed row, names arriving after the directory page can end up out of view: that list scrolls to its first item when name results arrive in one-hand mode ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L474-L480)). The `ChatList` branch starts with a keyless spacer, which stays the anchor.
|
||||
|
||||
| Row | View | Tap |
|
||||
|-----|------|-----|
|
||||
| Name | [`NameSearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L288-L316): avatar, profile name in bold, the SimpleX name in parentheses (secondary, one `AnnotatedString`, one line), description (2 lines) | `planAndConnect(rhId, result.target, close = null, cleanup = { withApi { refreshLocalChatIds() } })` |
|
||||
| Directory entry | [`DirectorySearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L241-L286): avatar, `#` + name, the directory's SimpleX name claim as plain text (truncates first), description (2 lines), member or subscriber count (`body2`, secondary colour), and the verb "Join channel" / "Join group" (one line, never wrapped) | `planAndConnect(rhId, result.entry.link, close = null, cleanup = { withApi { refreshLocalChatIds() } })` |
|
||||
| Retry | [`DirectoryRetryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L335-L347): "Search failed, tap to retry" | `withApi { retry() }` |
|
||||
| Show more | [`DirectoryShowMoreRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L318-L333): "Show more", disabled (secondary colour) while `loading` | `withApi { loadMore() }` |
|
||||
| Name | [`NameSearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L297-L325): avatar, profile name in bold, the SimpleX name in parentheses (secondary, one `AnnotatedString`, one line), description (2 lines) | `planAndConnect(rhId, result.target, close = null, cleanup = { withApi { refreshLocalChatIds() } })` |
|
||||
| Directory entry | [`DirectorySearchRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L250-L295): avatar, `#` + name, the directory's SimpleX name claim as plain text (truncates first), description (2 lines), member or subscriber count (`body2`, secondary colour), and the verb "Join channel" / "Join group" (one line, never wrapped) | `planAndConnect(rhId, result.entry.link, close = null, cleanup = { withApi { refreshLocalChatIds() } })` |
|
||||
| Known chat | When `result.localChatId` names a loaded chat (`chatModel.getChat`), in place of the directory entry: the chat list's own row, [`ChatListNavLinkView(chat, remember { mutableStateOf(false) })`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L540) | as a local chat: opens it |
|
||||
| Note | [`DirectoryPageInChatsRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L327-L336), an `item` after the directory rows when `rows.pageInChats` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L552-L554)): "All results on this page are already in your chats", secondary colour, padded as "Show more" | none |
|
||||
| Retry | [`DirectoryRetryRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L355-L367): "Search failed, tap to retry" | `withApi { retry() }` |
|
||||
| Show more | [`DirectoryShowMoreRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L338-L353): "Show more", disabled (secondary colour) while `loading` | `withApi { loadMore() }` |
|
||||
|
||||
A name or directory row tap first hides the keyboard (`hideKeyboard(view)`), then runs `planAndConnect` with `withBGApi`. Text from the directory and from name owners is rendered with `Text(String)` (never parsed as markdown) with line limits.
|
||||
|
||||
|
||||
@@ -386,23 +386,23 @@ The stage value is stored in `appPrefs.onboardingStage` and persisted across app
|
||||
|
||||
## 10. New Chat Online Search
|
||||
|
||||
**Location:** [`NewChatSheetLayout`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L110-L540)
|
||||
**Location:** [`NewChatSheetLayout`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L110-L548)
|
||||
|
||||
`NewChatSheetLayout` offers the chat list's online search (see [Chat List, Online Search](chat-list.md#11-online-search)) with its own [`OnlineSearchModel`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L141) (`remember`), reset from a `KeyChangeEffect` on `searchText.value.text` and another on `chatModel.currentUser.value?.userId` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L182-L187)). A profile switch also closes all modals on Kotlin, so the sheet is disposed anyway. The sheet's `DisposableEffect` `onDispose` ([`NewChatSheet`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L42-L48)) calls `cancelConnectProgress()`, which cancels a search still in flight. The model is also dropped while a modal opened from the sheet covers it (`ModalManager` composes only the top modal) and on an Android rotation.
|
||||
|
||||
| Element | Where | Behavior |
|
||||
|---------|-------|----------|
|
||||
| Search key | [`ContactsSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L560-L662) with `onSearch` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L568)), passed to `SearchTextField` | the keyboard's Search action (Android) or Enter (desktop) runs [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L242-L247): `search(text, name = connectNameCandidate.value)` |
|
||||
| "Connect to …" row | the sticky header, below the field, in both `OneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L392-L396)) and `NonOneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L490-L494)) | shown for a name candidate until name results arrive (`candidate != null && onlineSearch.names.isEmpty()`) |
|
||||
| "Search in Directory" row | after the "Connect to …" row, behind a `Divider` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L397-L400), [source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L495-L498)) | shown for non-empty, non-link text; runs [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L236-L240): `searchDirectory(text)` |
|
||||
| Names and Directory sections | after `itemsIndexed(filteredContactChats)` in both columns, [`onlineSearchSections(rows)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L196-L232) | each part starts with a `SectionDividerSpaced` item, then the chat list's `searchPart` places its [`OnlineSearchHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L189-L192) item (the `SectionView` title, not upper-cased), styled as "Your contacts"; rows are `items` keyed by `id`; the Directory part ends with the retry or "Show more" row; rows tap `hideKeyboard(view)` then `withBGApi { planAndConnect(rhId, …, close = close, cleanup = null) }`, which closes the sheet once connected |
|
||||
| Search key | [`ContactsSearchBar`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L568-L670) with `onSearch` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L576)), passed to `SearchTextField` | the keyboard's Search action (Android) or Enter (desktop) runs [`runOnlineSearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L250-L255): `search(text, name = connectNameCandidate.value)` |
|
||||
| "Connect to …" row | the sticky header, below the field, in both `OneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L400-L404)) and `NonOneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L498-L502)) | shown for a name candidate until name results arrive (`candidate != null && onlineSearch.names.isEmpty()`) |
|
||||
| "Search in Directory" row | after the "Connect to …" row, behind a `Divider` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L405-L408), [source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L503-L506)) | shown for non-empty, non-link text; runs [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L244-L248): `searchDirectory(text)` |
|
||||
| Names and Directory sections | after `itemsIndexed(filteredContactChats)` in both columns, [`onlineSearchSections(rows)`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L196-L240) | each part starts with a `SectionDividerSpaced` item, then the chat list's `searchPart` places its [`OnlineSearchHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L189-L192) item (the `SectionView` title, not upper-cased), styled as "Your contacts"; rows are `items` keyed by `id`; the Directory part renders when `rows.directory` is not empty, `rows.pageInChats` or `rows.footer` is set, and ends with the note item ([`DirectoryPageInChatsRow`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DirectorySearchView.kt#L327-L336), when `rows.pageInChats`) and then the retry or "Show more" row. A directory result whose `localChatId` names a loaded chat (`chatModel.getChat`) is drawn as `ContactListNavLinkView(chat, remember { mutableStateOf(false) }, showDeletedChatIcon = true)`, whose [`ChatInfo.Group` branch](../../common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt#L138-L156) opens the group and closes the sheet (`openChat` then `ModalManager.start.closeModals()`; `ContactPreviewView` draws the avatar, the name as a contact request's, favourite star and incognito mask; no menu items); other rows tap `hideKeyboard(view)` then `withBGApi { planAndConnect(rhId, …, close = close, cleanup = null) }`, which closes the sheet once connected |
|
||||
|
||||
Both triggers do nothing for empty text or a pasted link ([`searchTrimmed()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L234), `searchShowingSimplexLink`) and go through `confirmDirectorySearch`, launching the model with `withApi`. `onlineSearchSections`, `OnlineSearchHeader`, `searchTrimmed`, `runDirectorySearch` and `runOnlineSearch` are local functions of `NewChatSheetLayout`. The one-hand column is reversed, so there `searchPart` puts each header after its rows and it is drawn above them, as in the chat list. A reversed list also reverses the nodes inside an item, so the divider is an item of its own: drawn below its part's rows there, it separates the parts, where in the header's item it would fall between the header and its rows. The contacts header shows only while the search text is empty, so it is never next to online results.
|
||||
Both triggers do nothing for empty text or a pasted link ([`searchTrimmed()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L242), `searchShowingSimplexLink`) and go through `confirmDirectorySearch`, launching the model with `withApi`. `onlineSearchSections`, `OnlineSearchHeader`, `searchTrimmed`, `runDirectorySearch` and `runOnlineSearch` are local functions of `NewChatSheetLayout`. The one-hand column is reversed, so there `searchPart` puts each header after its rows and it is drawn above them, as in the chat list. A reversed list also reverses the nodes inside an item, so the divider is an item of its own: drawn below its part's rows there, it separates the parts, where in the header's item it would fall between the header and its rows. The contacts header shows only while the search text is empty, so it is never next to online results.
|
||||
|
||||
**De-duplication against the contacts list.** `filteredContactChats` (the contacts the list shows, computed in the layout's scope) gives the shown ids: `rows = onlineSearch.rows(shownChatIds = filteredContactChats.map { it.id }.toSet())` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L180)), so no extra helper is needed.
|
||||
|
||||
**Empty state.** `showsEmptyState = !onlineSearch.searching && rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L181)); [`NoFilteredContactsItem`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L329-L343) shows "No filtered contacts" only when it holds. Kotlin has no "No contacts" state.
|
||||
**Empty state.** `showsEmptyState = !onlineSearch.searching && rows.isEmpty` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L181)); [`NoFilteredContactsItem`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L337-L351) shows "No filtered contacts" only when it holds. Kotlin has no "No contacts" state.
|
||||
|
||||
**Archived contacts.** [`DeletedContactsView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L755-L869) passes no `onSearch` to `ContactsSearchBar`: its field keeps the Search IME label (every `SearchTextField` uses `ImeAction.Search`), the key does nothing, and it shows no online rows; its empty state is unchanged.
|
||||
**Archived contacts.** [`DeletedContactsView`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L763-L877) passes no `onSearch` to `ContactsSearchBar`: its field keeps the Search IME label (every `SearchTextField` uses `ImeAction.Search`), the key does nothing, and it shows no online rows; its empty state is unchanged.
|
||||
|
||||
[GAP] New chat does not resolve a typed name locally while typing (the `ContactsSearchBar` collector resolves a name only when its "Connect to …" row is tapped), so a known contact whose name does not contain the typed text is not in "Your contacts" and appears as a Names row; tapping it opens the existing chat. See [GAP-11](../../product/gaps.md#gap-11-online-search-new-chat-does-not-narrow-contacts-by-name-while-typing).
|
||||
Reference in new issue
Block a user