mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 08:02:24 +00:00
android, desktop: show the Names and Directory headers above their results in the New chat sheet with the one-hand UI
This commit is contained in:
committed by
spaced4ndy
parent
b402bf4b3e
commit
48ca4d11ac
+3
-3
@@ -554,12 +554,12 @@ private fun LazyListScope.directoryRows(rows: OnlineSearchRows, onlineSearch: On
|
||||
}
|
||||
|
||||
// the list is reversed in one-hand UI, so there the header follows its rows to be drawn above them
|
||||
private fun LazyListScope.searchPart(title: StringResource, oneHandUI: Boolean, content: LazyListScope.() -> Unit) {
|
||||
internal fun LazyListScope.searchPart(title: StringResource, oneHandUI: Boolean, header: @Composable (String) -> Unit = { SearchPartHeader(it) }, content: LazyListScope.() -> Unit) {
|
||||
if (oneHandUI) {
|
||||
content()
|
||||
item { SearchPartHeader(stringResource(title)) }
|
||||
item { header(stringResource(title)) }
|
||||
} else {
|
||||
item { SearchPartHeader(stringResource(title)) }
|
||||
item { header(stringResource(title)) }
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
+31
-26
@@ -188,40 +188,45 @@ private fun ModalData.NewChatSheetLayout(
|
||||
|
||||
@Composable
|
||||
fun OnlineSearchHeader(title: String) {
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
SectionView(title, headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
}
|
||||
|
||||
// the divider is an item of its own: the reversed one-hand column also reverses the nodes inside an item,
|
||||
// which would put it between the header and its rows
|
||||
fun LazyListScope.onlineSearchSections(rows: OnlineSearchRows) {
|
||||
if (rows.names.isNotEmpty()) {
|
||||
item { OnlineSearchHeader(stringResource(MR.strings.online_search_names)) }
|
||||
items(rows.names, key = { it.id }) { result ->
|
||||
val view = LocalMultiplatformView()
|
||||
NameSearchRow(
|
||||
result,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.target, close = close, cleanup = null) }
|
||||
}
|
||||
)
|
||||
item { SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) }
|
||||
searchPart(MR.strings.online_search_names, oneHandUI.value, header = { OnlineSearchHeader(it) }) {
|
||||
items(rows.names, key = { it.id }) { result ->
|
||||
val view = LocalMultiplatformView()
|
||||
NameSearchRow(
|
||||
result,
|
||||
Modifier.clickable {
|
||||
hideKeyboard(view)
|
||||
withBGApi { planAndConnect(chatModel.remoteHostId(), result.target, close = close, cleanup = null) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rows.directory.isNotEmpty() || rows.footer != null) {
|
||||
item { OnlineSearchHeader(stringResource(MR.strings.online_search_directory)) }
|
||||
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) }
|
||||
}
|
||||
)
|
||||
}
|
||||
when (rows.footer) {
|
||||
DirectoryFooter.Retry -> item { DirectoryRetryRow(onlineSearch) }
|
||||
DirectoryFooter.ShowMore -> item { DirectoryShowMoreRow(onlineSearch) }
|
||||
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) }
|
||||
}
|
||||
)
|
||||
}
|
||||
when (rows.footer) {
|
||||
DirectoryFooter.Retry -> item { DirectoryRetryRow(onlineSearch) }
|
||||
DirectoryFooter.ShowMore -> item { DirectoryShowMoreRow(onlineSearch) }
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, so a header is shown below its rows, as "Your contacts" is |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -468,7 +468,7 @@ Only store-derived chat ids and the link the core resolved can hide a row, never
|
||||
|
||||
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#L1237-L1239)) 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. 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 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.
|
||||
|
||||
| Row | View | Tap |
|
||||
|-----|------|-----|
|
||||
|
||||
@@ -385,23 +385,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-L535)
|
||||
**Location:** [`NewChatSheetLayout`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L110-L540)
|
||||
|
||||
`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#L555-L657) with `onSearch` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L563)), 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#L237-L242): `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#L387-L391)) and `NonOneHandLazyColumn` ([source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L485-L489)) | 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#L392-L395), [source](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L490-L493)) | shown for non-empty, non-link text; runs [`runDirectorySearch()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L231-L235): `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#L195-L227) | each part starts with an [`OnlineSearchHeader`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L189-L193) item styled as "Your contacts" (`SectionDividerSpaced` then the `SectionView` title, not upper-cased); 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#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 |
|
||||
|
||||
Both triggers do nothing for empty text or a pasted link ([`searchTrimmed()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt#L229), `searchShowingSimplexLink`) and go through `confirmDirectorySearch`, launching the model with `withApi`. `onlineSearchSections`, `OnlineSearchHeader`, `searchTrimmed`, `runDirectorySearch` and `runOnlineSearch` are local functions of `NewChatSheetLayout`. In the reversed one-hand column a header item renders below its rows, as the contacts header does.
|
||||
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.
|
||||
|
||||
**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#L324-L338) 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#L329-L343) 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#L750-L864) 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#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.
|
||||
|
||||
[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-10](../../product/gaps.md#gap-10-online-search-new-chat-does-not-narrow-contacts-by-name-while-typing).
|
||||
|
||||
Reference in New Issue
Block a user