From ecdaad06ff429747c68b620d94c3dbdd6911b6d5 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 11 Jun 2018 13:33:21 -0400 Subject: [PATCH 1/3] Handle no results In line with other messaging apps, we intentionally don't show a "No Results" cell. We simply don't display any cells. Though we could easily modify this in the future. // FREEBIE --- .../ConversationSearchViewController.swift | 30 ++++++++++++------- .../translations/en.lproj/Localizable.strings | 3 -- .../utils/ConversationSearcher.swift | 4 +++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index bea9df6b4..a1853916b 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 @@ -41,39 +42,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 +91,9 @@ class ConversationSearchViewController: UITableViewController { } switch searchSection { + case .noResults: + // We don't display a "no results" cell, we simply display no results. + return 0 case .conversations: return searchResultSet.conversations.count case .contacts: @@ -104,6 +110,8 @@ class ConversationSearchViewController: UITableViewController { } switch searchSection { + case .noResults: + return UITableViewCell() case .conversations: guard let cell = tableView.dequeueReusableCell(withIdentifier: ConversationSearchResultCell.reuseIdentifier) as? ConversationSearchResultCell else { return UITableViewCell() @@ -150,6 +158,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)") diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 10a9e08af..57428e1c6 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -986,9 +986,6 @@ /* A label for conversations with blocked users. */ "HOME_VIEW_BLOCKED_CONTACT_CONVERSATION" = "Blocked"; -/* Placeholder text for search bar which filters conversations. */ -"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER" = "Search"; - /* 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..91f01d11d 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -40,6 +40,10 @@ public class SearchResultSet { public class var empty: SearchResultSet { return SearchResultSet(conversations: [], contacts: [], messages: []) } + + public var isEmpty: Bool { + return conversations.isEmpty && contacts.isEmpty && messages.isEmpty + } } @objc From 98983ac8e6cce76bff4d761208218a5b6d36570f Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 11 Jun 2018 14:00:17 -0400 Subject: [PATCH 2/3] Localize search bar // FREEBIE --- Signal/translations/en.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 57428e1c6..10a9e08af 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -986,6 +986,9 @@ /* A label for conversations with blocked users. */ "HOME_VIEW_BLOCKED_CONTACT_CONVERSATION" = "Blocked"; +/* Placeholder text for search bar which filters conversations. */ +"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER" = "Search"; + /* Title for the home view's 'archive' mode. */ "HOME_VIEW_TITLE_ARCHIVE" = "Archive"; From 549342c702b90e50e275a930f09ed9af1f17fe78 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 11 Jun 2018 14:24:29 -0400 Subject: [PATCH 3/3] Show empty results text // FREEBIE --- .../ConversationSearchViewController.swift | 65 +++++++++++++++++-- .../translations/en.lproj/Localizable.strings | 3 + .../utils/ConversationSearcher.swift | 8 ++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index a1853916b..46f5b99b0 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -33,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) @@ -92,8 +93,7 @@ class ConversationSearchViewController: UITableViewController { switch searchSection { case .noResults: - // We don't display a "no results" cell, we simply display no results. - return 0 + return searchResultSet.isEmpty ? 1 : 0 case .conversations: return searchResultSet.conversations.count case .contacts: @@ -111,23 +111,39 @@ class ConversationSearchViewController: UITableViewController { switch searchSection { case .noResults: - return UITableViewCell() + 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() } @@ -135,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() } @@ -148,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? { @@ -349,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 91f01d11d..32f4bab1d 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -27,18 +27,20 @@ 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 { @@ -94,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:)