diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index a64c07b427..b34ce883db 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -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() }