ios: search names and the directory from New chat

The New chat sheet gets the chat list's online search: a "Search in
Directory" row, the keyboard Search key for names and the directory,
and Names and Directory sections after the contacts. Tapping a result
connects and closes the sheet, as other New chat actions do. A contact
the list already shows is not repeated as a name result, and the
contacts list's empty state stays hidden while a search runs or shows
results. Archived contacts, which share the search bar and the list,
are unchanged.

Run: 002-ios-directory-search-ui, slice 4
This commit is contained in:
spaced4ndy
2026-10-13 11:46:00 +00:00
parent bfb62d2606
commit eef174ea60
@@ -43,6 +43,7 @@ struct NewChatSheet: View {
@State private var searchChatFilteredBySimplexLink: String? = nil
// when the search text is a SimpleX name, the string to connect to (with @/# preserved); nil otherwise
@State private var connectNameCandidate: String? = nil
@StateObject private var onlineSearch = OnlineSearchModel()
@State private var alert: SomeAlert?
// Sheet height management
@@ -52,6 +53,7 @@ struct NewChatSheet: View {
@State private var allowSmallSheet = true
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
let showArchive = chatModel.chats.contains { $0.chatInfo.contact?.chatDeleted == true }
@@ -90,10 +92,11 @@ struct NewChatSheet: View {
searchText: $searchText,
searchShowingSimplexLink: $searchShowingSimplexLink,
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink,
connectNameCandidate: $connectNameCandidate
connectNameCandidate: $connectNameCandidate,
onSearch: runOnlineSearch
)
.frame(maxWidth: .infinity)
if let candidate = connectNameCandidate {
if let candidate = connectNameCandidate, onlineSearch.names.isEmpty {
ConnectByNameRow(
name: candidate,
searchText: $searchText,
@@ -102,6 +105,9 @@ struct NewChatSheet: View {
dismiss: true
)
}
if !searchTrimmed.isEmpty, !searchShowingSimplexLink {
SearchInDirectoryRow(searchFocussed: $searchFocussed, onSearch: runDirectorySearch)
}
}
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
@@ -155,7 +161,16 @@ struct NewChatSheet: View {
}
}
}
let shownContacts = ContactsList.shownChats(
chats: chatModel.chats,
predicate: contactListChatPredicate,
searchText: searchText,
searchShowingSimplexLink: searchShowingSimplexLink,
searchChatFilteredBySimplexLink: searchChatFilteredBySimplexLink,
showUnreadAndFavorites: showUnreadAndFavorites
).shown
let rows = onlineSearch.rows(shownChatIds: Set(shownContacts.map { $0.id }))
ContactsList(
chatPredicate: contactListChatPredicate,
searchMode: $searchMode,
@@ -164,9 +179,71 @@ struct NewChatSheet: View {
searchFocussed: $searchFocussed,
searchShowingSimplexLink: $searchShowingSimplexLink,
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink,
showDeletedChatIcon: true
showDeletedChatIcon: true,
showsEmptyState: !onlineSearch.searching && rows.isEmpty
)
onlineSearchSections(rows)
}
.onChange(of: searchText) { _ in
onlineSearch.reset()
}
}
@ViewBuilder private func onlineSearchSections(_ rows: OnlineSearchRows) -> some View {
if !rows.names.isEmpty {
Section {
ForEach(rows.names) { result in
NameSearchRow(result: result)
.onTapGesture {
searchFocussed = false
planAndConnect(result.target, theme: theme, dismiss: true, cleanup: nil)
}
}
} header: {
onlineSearchHeader("Names")
}
}
if !rows.directory.isEmpty || rows.footer != nil {
Section {
ForEach(rows.directory) { result in
DirectorySearchRow(entry: result.entry)
.onTapGesture {
searchFocussed = false
planAndConnect(result.entry.link, theme: theme, dismiss: true, cleanup: nil)
}
}
switch rows.footer {
case .retry: DirectoryRetryRow(onlineSearch: onlineSearch)
case .showMore: DirectoryShowMoreRow(onlineSearch: onlineSearch)
case nil: EmptyView()
}
} header: {
onlineSearchHeader("Directory")
}
}
}
private func onlineSearchHeader(_ title: LocalizedStringKey) -> some View {
Text(title)
.textCase(.uppercase)
.foregroundColor(theme.colors.secondary)
}
private var searchTrimmed: String {
searchText.trimmingCharacters(in: .whitespaces)
}
private func runDirectorySearch() {
let text = searchTrimmed
guard !text.isEmpty, !searchShowingSimplexLink else { return }
confirmDirectorySearch { Task { await onlineSearch.searchDirectory(text) } }
}
private func runOnlineSearch() {
let text = searchTrimmed
guard !text.isEmpty, !searchShowingSimplexLink else { return }
let name = connectNameCandidate
confirmDirectorySearch { Task { await onlineSearch.search(text, name: name) } }
}
private func contactListChatPredicate(_ chat: Chat, _ withSearch: Bool) -> Bool {
@@ -231,8 +308,9 @@ struct ContactsList: View {
@Binding var searchShowingSimplexLink: Bool
@Binding var searchChatFilteredBySimplexLink: String?
var showDeletedChatIcon: Bool
var showsEmptyState = true
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
let (contactChats, filteredContactChats) = Self.shownChats(
chats: chatModel.chats,
@@ -259,10 +337,12 @@ struct ContactsList: View {
}
}
if filteredContactChats.isEmpty && !contactChats.isEmpty {
noResultSection(text: "No filtered contacts")
} else if contactChats.isEmpty {
noResultSection(text: "No contacts")
if showsEmptyState {
if filteredContactChats.isEmpty && !contactChats.isEmpty {
noResultSection(text: "No filtered contacts")
} else if contactChats.isEmpty {
noResultSection(text: "No contacts")
}
}
}
@@ -359,6 +439,7 @@ struct ContactsListSearchBar: View {
@Binding var searchShowingSimplexLink: Bool
@Binding var searchChatFilteredBySimplexLink: String?
@Binding var connectNameCandidate: String?
var onSearch: (() -> Void)? = nil
@State private var ignoreSearchTextChange = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@@ -376,6 +457,8 @@ struct ContactsListSearchBar: View {
.disabled(searchShowingSimplexLink)
.focused($searchFocussed)
.frame(maxWidth: .infinity)
.submitLabel(onSearch == nil ? .return : .search)
.onSubmit { onSearch?() }
if connectProgressManager.showConnectProgress != nil {
ProgressView()
}