diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index bea9df6b4..46f5b99b0 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -19,9 +19,10 @@ class ConversationSearchViewController: UITableViewController { } enum SearchSection: Int { - case conversations = 0 - case contacts = 1 - case messages = 2 + case noResults + case conversations + case contacts + case messages } // MARK: View Lifecyle @@ -32,6 +33,7 @@ class ConversationSearchViewController: UITableViewController { tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 60 + tableView.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier) tableView.register(ConversationSearchResultCell.self, forCellReuseIdentifier: ConversationSearchResultCell.reuseIdentifier) tableView.register(MessageSearchResultCell.self, forCellReuseIdentifier: MessageSearchResultCell.reuseIdentifier) tableView.register(ContactSearchResultCell.self, forCellReuseIdentifier: ContactSearchResultCell.reuseIdentifier) @@ -41,39 +43,41 @@ class ConversationSearchViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) - + guard let searchSection = SearchSection(rawValue: indexPath.section) else { owsFail("\(logTag) unknown section selected.") return } - + switch searchSection { + case .noResults: + owsFail("\(logTag) shouldn't be able to tap 'no results' section") case .conversations: let sectionResults = searchResultSet.conversations guard let searchResult = sectionResults[safe: indexPath.row] else { owsFail("\(logTag) unknown row selected.") return } - + let thread = searchResult.thread SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose) - + case .contacts: let sectionResults = searchResultSet.contacts guard let searchResult = sectionResults[safe: indexPath.row] else { owsFail("\(logTag) unknown row selected.") return } - + SignalApp.shared().presentConversation(forRecipientId: searchResult.recipientId, action: .compose) - + case .messages: let sectionResults = searchResultSet.messages guard let searchResult = sectionResults[safe: indexPath.row] else { owsFail("\(logTag) unknown row selected.") return } - + let thread = searchResult.thread SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose) } @@ -88,6 +92,8 @@ class ConversationSearchViewController: UITableViewController { } switch searchSection { + case .noResults: + return searchResultSet.isEmpty ? 1 : 0 case .conversations: return searchResultSet.conversations.count case .contacts: @@ -104,22 +110,40 @@ class ConversationSearchViewController: UITableViewController { } switch searchSection { + case .noResults: + guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell else { + owsFail("cell was unexpectedly nil") + return UITableViewCell() + } + + guard indexPath.row == 0 else { + owsFail("searchResult was unexpected index") + return UITableViewCell() + } + + let searchText = self.searchResultSet.searchText + cell.configure(searchText: searchText) + return cell case .conversations: guard let cell = tableView.dequeueReusableCell(withIdentifier: ConversationSearchResultCell.reuseIdentifier) as? ConversationSearchResultCell else { + owsFail("cell was unexpectedly nil") return UITableViewCell() } guard let searchResult = self.searchResultSet.conversations[safe: indexPath.row] else { + owsFail("searchResult was unexpectedly nil") return UITableViewCell() } cell.configure(searchResult: searchResult) return cell case .contacts: guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactSearchResultCell.reuseIdentifier) as? ContactSearchResultCell else { + owsFail("cell was unexpectedly nil") return UITableViewCell() } guard let searchResult = self.searchResultSet.contacts[safe: indexPath.row] else { + owsFail("searchResult was unexpectedly nil") return UITableViewCell() } @@ -127,10 +151,12 @@ class ConversationSearchViewController: UITableViewController { return cell case .messages: guard let cell = tableView.dequeueReusableCell(withIdentifier: MessageSearchResultCell.reuseIdentifier) as? MessageSearchResultCell else { + owsFail("cell was unexpectedly nil") return UITableViewCell() } guard let searchResult = self.searchResultSet.messages[safe: indexPath.row] else { + owsFail("searchResult was unexpectedly nil") return UITableViewCell() } @@ -140,7 +166,7 @@ class ConversationSearchViewController: UITableViewController { } override func numberOfSections(in tableView: UITableView) -> Int { - return 3 + return 4 } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { @@ -150,6 +176,8 @@ class ConversationSearchViewController: UITableViewController { } switch searchSection { + case .noResults: + return nil case .conversations: if searchResultSet.conversations.count > 0 { return NSLocalizedString("SEARCH_SECTION_CONVERSATIONS", comment: "section header for search results that match existing conversations (either group or contact conversations)") @@ -339,3 +367,42 @@ class ContactSearchResultCell: UITableViewCell { self.snippetLabel.text = searchResult.recipientId } } + +class EmptySearchResultCell: UITableViewCell { + static let reuseIdentifier = "EmptySearchResultCell" + + let messageLabel: UILabel + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + self.messageLabel = UILabel() + super.init(style: style, reuseIdentifier: reuseIdentifier) + + messageLabel.font = UIFont.ows_dynamicTypeBody + messageLabel.textAlignment = .center + messageLabel.numberOfLines = 3 + + contentView.addSubview(messageLabel) + + messageLabel.autoSetDimension(.height, toSize: 150) + + messageLabel.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual) + messageLabel.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual) + messageLabel.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual) + messageLabel.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual) + + messageLabel.autoVCenterInSuperview() + messageLabel.autoHCenterInSuperview() + + messageLabel.setContentHuggingHigh() + messageLabel.setCompressionResistanceHigh() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func configure(searchText: String) { + let format = NSLocalizedString("HOME_VIEW_SEARCH_NO_RESULTS_FORMAT", comment: "Format string when search returns no results. Embeds {{search term}}") + let messageText: String = NSString(format: format as NSString, searchText) as String + self.messageLabel.text = messageText + } +} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 10a9e08af..a2cfc385e 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -989,6 +989,9 @@ /* Placeholder text for search bar which filters conversations. */ "HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER" = "Search"; +/* Format string when search returns no results. Embeds {{search term}} */ +"HOME_VIEW_SEARCH_NO_RESULTS_FORMAT" = "No results found for '%@'"; + /* Title for the home view's 'archive' mode. */ "HOME_VIEW_TITLE_ARCHIVE" = "Archive"; diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index c38abd771..32f4bab1d 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -27,18 +27,24 @@ public class ContactSearchResult { } public class SearchResultSet { + public let searchText: String public let conversations: [ConversationSearchResult] public let contacts: [ContactSearchResult] public let messages: [ConversationSearchResult] - public init(conversations: [ConversationSearchResult], contacts: [ContactSearchResult], messages: [ConversationSearchResult]) { + public init(searchText: String, conversations: [ConversationSearchResult], contacts: [ContactSearchResult], messages: [ConversationSearchResult]) { + self.searchText = searchText self.conversations = conversations self.contacts = contacts self.messages = messages } public class var empty: SearchResultSet { - return SearchResultSet(conversations: [], contacts: [], messages: []) + return SearchResultSet(searchText: "", conversations: [], contacts: [], messages: []) + } + + public var isEmpty: Bool { + return conversations.isEmpty && contacts.isEmpty && messages.isEmpty } } @@ -90,7 +96,7 @@ public class ConversationSearcher: NSObject { // Only show contacts which were not included in an existing 1:1 conversation. let otherContacts: [ContactSearchResult] = contacts.filter { !existingConversationRecipientIds.contains($0.recipientId) } - return SearchResultSet(conversations: conversations, contacts: otherContacts, messages: messages) + return SearchResultSet(searchText: searchText, conversations: conversations, contacts: otherContacts, messages: messages) } @objc(filterThreads:withSearchText:)