session-ios/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift

445 lines
16 KiB
Swift
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
protocol ConversationSearchViewDelegate: class {
func conversationSearchViewWillBeginDragging()
}
@objc
class ConversationSearchViewController: UITableViewController {
@objc
public weak var delegate: ConversationSearchViewDelegate?
2018-06-13 17:37:01 +02:00
@objc
public var searchText = "" {
didSet {
AssertIsOnMainThread()
2018-06-13 17:37:01 +02:00
2018-06-13 17:46:23 +02:00
// Use a slight delay to debounce updates.
2018-06-13 19:02:20 +02:00
refreshSearchResults()
2018-06-13 17:37:01 +02:00
}
}
var searchResultSet: SearchResultSet = SearchResultSet.empty
var uiDatabaseConnection: YapDatabaseConnection {
return OWSPrimaryStorage.shared().uiDatabaseConnection
}
var searcher: ConversationSearcher {
return ConversationSearcher.shared
}
2018-06-12 21:12:14 +02:00
private var contactsManager: OWSContactsManager {
return Environment.current().contactsManager
}
enum SearchSection: Int {
case noResults
case conversations
case contacts
case messages
}
2018-06-12 17:27:32 +02:00
var blockedPhoneNumberSet = Set<String>()
2018-07-23 21:03:07 +02:00
private var hasThemeChanged = false
2018-06-13 17:37:01 +02:00
// MARK: View Lifecycle
2018-06-13 15:54:18 +02:00
override func viewDidLoad() {
super.viewDidLoad()
2018-06-12 17:27:32 +02:00
let blockingManager = OWSBlockingManager.shared()
blockedPhoneNumberSet = Set(blockingManager.blockedPhoneNumbers())
2018-06-09 07:00:07 +02:00
tableView.rowHeight = UITableViewAutomaticDimension
2018-06-15 17:08:01 +02:00
tableView.estimatedRowHeight = 60
2018-08-22 22:30:12 +02:00
tableView.separatorColor = Theme.cellSeparatorColor
2018-06-09 07:00:07 +02:00
2018-06-11 20:24:29 +02:00
tableView.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier)
2018-06-12 17:27:32 +02:00
tableView.register(HomeViewCell.self, forCellReuseIdentifier: HomeViewCell.cellReuseIdentifier())
tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: ContactTableViewCell.reuseIdentifier())
2018-06-13 17:37:01 +02:00
NotificationCenter.default.addObserver(self,
selector: #selector(yapDatabaseModified),
name: NSNotification.Name.YapDatabaseModified,
object: OWSPrimaryStorage.shared().dbNotificationObject)
2018-07-23 21:03:07 +02:00
NotificationCenter.default.addObserver(self,
selector: #selector(themeDidChange),
name: NSNotification.Name.ThemeDidChange,
object: nil)
applyTheme()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard hasThemeChanged else {
return
}
hasThemeChanged = false
applyTheme()
self.tableView.reloadData()
}
deinit {
NotificationCenter.default.removeObserver(self)
2018-06-13 17:37:01 +02:00
}
@objc internal func yapDatabaseModified(notification: NSNotification) {
AssertIsOnMainThread()
2018-06-13 17:37:01 +02:00
2018-06-13 19:02:20 +02:00
refreshSearchResults()
}
2018-07-23 21:03:07 +02:00
@objc internal func themeDidChange(notification: NSNotification) {
AssertIsOnMainThread()
2018-07-23 21:03:07 +02:00
applyTheme()
self.tableView.reloadData()
hasThemeChanged = true
}
private func applyTheme() {
AssertIsOnMainThread()
2018-07-23 21:03:07 +02:00
self.view.backgroundColor = Theme.backgroundColor
self.tableView.backgroundColor = Theme.backgroundColor
}
2018-06-11 18:15:46 +02:00
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
2018-06-11 18:15:46 +02:00
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
owsFail("\(logTag) unknown section selected.")
return
}
2018-06-11 18:15:46 +02:00
switch searchSection {
case .noResults:
owsFail("\(logTag) shouldn't be able to tap 'no results' section")
2018-06-11 18:15:46 +02:00
case .conversations:
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet.conversations
guard let searchResult = sectionResults[safe: indexPath.row] else {
owsFail("\(logTag) unknown row selected.")
return
}
2018-06-11 19:51:15 +02:00
let thread = searchResult.thread
SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose)
2018-06-11 18:15:46 +02:00
case .contacts:
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet.contacts
guard let searchResult = sectionResults[safe: indexPath.row] else {
owsFail("\(logTag) unknown row selected.")
return
}
2018-06-11 19:51:15 +02:00
SignalApp.shared().presentConversation(forRecipientId: searchResult.recipientId, action: .compose)
2018-06-11 18:15:46 +02:00
case .messages:
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet.messages
guard let searchResult = sectionResults[safe: indexPath.row] else {
owsFail("\(logTag) unknown row selected.")
return
}
2018-06-11 19:51:15 +02:00
let thread = searchResult.thread
2018-06-11 21:31:54 +02:00
SignalApp.shared().presentConversation(for: thread.threadRecord,
action: .compose,
focusMessageId: searchResult.messageId)
2018-06-11 18:15:46 +02:00
}
}
2018-06-09 07:00:07 +02:00
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let searchSection = SearchSection(rawValue: section) else {
owsFail("unknown section: \(section)")
return 0
}
switch searchSection {
case .noResults:
2018-06-11 20:24:29 +02:00
return searchResultSet.isEmpty ? 1 : 0
case .conversations:
return searchResultSet.conversations.count
case .contacts:
return searchResultSet.contacts.count
case .messages:
return searchResultSet.messages.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
return UITableViewCell()
}
switch searchSection {
case .noResults:
2018-06-11 20:24:29 +02:00
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()
}
2018-07-23 21:03:07 +02:00
OWSTableItem.configureCell(cell)
2018-06-11 20:24:29 +02:00
let searchText = self.searchResultSet.searchText
cell.configure(searchText: searchText)
return cell
case .conversations:
2018-06-12 17:27:32 +02:00
guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeViewCell.cellReuseIdentifier()) as? HomeViewCell else {
2018-06-11 20:24:29 +02:00
owsFail("cell was unexpectedly nil")
return UITableViewCell()
}
guard let searchResult = self.searchResultSet.conversations[safe: indexPath.row] else {
2018-06-11 20:24:29 +02:00
owsFail("searchResult was unexpectedly nil")
return UITableViewCell()
}
2018-06-12 17:27:32 +02:00
cell.configure(withThread: searchResult.thread, contactsManager: contactsManager, blockedPhoneNumber: self.blockedPhoneNumberSet)
return cell
case .contacts:
2018-06-12 17:27:32 +02:00
guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier()) as? ContactTableViewCell else {
2018-06-11 20:24:29 +02:00
owsFail("cell was unexpectedly nil")
2018-06-11 18:20:49 +02:00
return UITableViewCell()
}
guard let searchResult = self.searchResultSet.contacts[safe: indexPath.row] else {
2018-06-11 20:24:29 +02:00
owsFail("searchResult was unexpectedly nil")
2018-06-11 18:20:49 +02:00
return UITableViewCell()
}
cell.configure(withRecipientId: searchResult.signalAccount.recipientId, contactsManager: contactsManager)
2018-06-11 18:20:49 +02:00
return cell
case .messages:
2018-06-12 17:27:32 +02:00
guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeViewCell.cellReuseIdentifier()) as? HomeViewCell else {
2018-06-11 20:24:29 +02:00
owsFail("cell was unexpectedly nil")
2018-06-08 19:23:17 +02:00
return UITableViewCell()
}
guard let searchResult = self.searchResultSet.messages[safe: indexPath.row] else {
2018-06-11 20:24:29 +02:00
owsFail("searchResult was unexpectedly nil")
return UITableViewCell()
2018-06-08 19:23:17 +02:00
}
2018-06-12 17:27:32 +02:00
var overrideSnippet = NSAttributedString()
2018-06-13 16:23:12 +02:00
var overrideDate: Date?
if searchResult.messageId != nil {
if let messageDate = searchResult.messageDate {
overrideDate = messageDate
2018-06-13 15:54:18 +02:00
} else {
2018-08-15 16:24:29 +02:00
owsFail("\(ConversationSearchViewController.logTag()) message search result is missing message timestamp")
2018-06-12 17:27:32 +02:00
}
2018-06-13 15:54:18 +02:00
// Note that we only use the snippet for message results,
// not conversation results. HomeViewCell will generate
// a snippet for conversations that reflects the latest
// contents.
if let messageSnippet = searchResult.snippet {
2018-08-07 23:12:16 +02:00
overrideSnippet = NSAttributedString(string: messageSnippet,
attributes: [
2018-08-16 23:27:20 +02:00
NSAttributedStringKey.foregroundColor: Theme.secondaryColor
2018-08-07 23:12:16 +02:00
])
2018-06-13 15:54:18 +02:00
} else {
2018-08-15 16:24:29 +02:00
owsFail("\(ConversationSearchViewController.logTag()) message search result is missing message snippet")
2018-06-12 17:27:32 +02:00
}
}
cell.configure(withThread: searchResult.thread,
contactsManager: contactsManager,
blockedPhoneNumber: self.blockedPhoneNumberSet,
2018-06-13 16:23:12 +02:00
overrideSnippet: overrideSnippet,
overrideDate: overrideDate)
2018-06-12 17:27:32 +02:00
2018-06-08 19:23:17 +02:00
return cell
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
2018-06-11 20:24:29 +02:00
return 4
}
2018-08-16 23:27:20 +02:00
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
2018-08-22 22:30:12 +02:00
guard nil != self.tableView(tableView, titleForHeaderInSection: section) else {
2018-08-16 23:27:20 +02:00
return 0
}
2018-08-22 22:30:12 +02:00
return UITableViewAutomaticDimension
2018-08-16 23:27:20 +02:00
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let title = self.tableView(tableView, titleForHeaderInSection: section) else {
return nil
}
let label = UILabel()
label.textColor = Theme.secondaryColor
label.text = title
label.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
label.tag = section
let hMargin: CGFloat = 15
let vMargin: CGFloat = 4
let wrapper = UIView()
wrapper.backgroundColor = Theme.offBackgroundColor
wrapper.addSubview(label)
label.autoPinWidthToSuperview(withMargin: hMargin)
label.autoPinHeightToSuperview(withMargin: vMargin)
return wrapper
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let searchSection = SearchSection(rawValue: section) else {
owsFail("unknown section: \(section)")
return nil
}
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)")
} else {
return nil
}
case .contacts:
if searchResultSet.contacts.count > 0 {
return NSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "section header for search results that match a contact who doesn't have an existing conversation")
} else {
return nil
}
case .messages:
if searchResultSet.messages.count > 0 {
return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "section header for search results that match a message in a conversation")
} else {
return nil
}
}
}
2018-06-13 17:46:23 +02:00
// MARK: Update Search Results
var refreshTimer: Timer?
2018-06-13 19:02:20 +02:00
private func refreshSearchResults() {
AssertIsOnMainThread()
2018-06-13 17:46:23 +02:00
2018-06-13 19:02:20 +02:00
guard !searchResultSet.isEmpty else {
// To avoid incorrectly showing the "no results" state,
// always search immediately if the current result set is empty.
refreshTimer?.invalidate()
refreshTimer = nil
updateSearchResults(searchText: searchText)
return
}
2018-06-13 17:46:23 +02:00
if refreshTimer != nil {
// Don't start a new refresh timer if there's already one active.
return
}
refreshTimer?.invalidate()
2018-06-13 19:02:20 +02:00
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
2018-06-13 17:46:23 +02:00
guard let strongSelf = self else {
return
}
strongSelf.updateSearchResults(searchText: strongSelf.searchText)
strongSelf.refreshTimer = nil
}
}
2018-06-13 17:37:01 +02:00
private func updateSearchResults(searchText: String) {
guard searchText.stripped.count > 0 else {
self.searchResultSet = SearchResultSet.empty
2018-06-12 21:12:14 +02:00
self.tableView.reloadData()
return
}
var searchResults: SearchResultSet?
2018-08-22 19:17:56 +02:00
self.uiDatabaseConnection.asyncRead({[weak self] transaction in
guard let strongSelf = self else { return }
searchResults = strongSelf.searcher.results(searchText: searchText, transaction: transaction, contactsManager: strongSelf.contactsManager)
},
2018-08-22 19:17:56 +02:00
completionBlock: { [weak self] in
AssertIsOnMainThread()
2018-08-22 19:17:56 +02:00
guard let strongSelf = self else { return }
guard let results = searchResults else {
2018-08-22 19:17:56 +02:00
owsFail("\(strongSelf.logTag) in \(#function) searchResults was unexpectedly nil")
return
}
2018-08-22 19:17:56 +02:00
strongSelf.searchResultSet = results
strongSelf.tableView.reloadData()
})
}
// MARK: - UIScrollViewDelegate
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
delegate?.conversationSearchViewWillBeginDragging()
}
2018-06-09 07:00:07 +02:00
}
2018-06-11 20:24:29 +02:00
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.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
2018-08-22 20:21:19 +02:00
messageLabel.textColor = Theme.primaryColor
messageLabel.font = UIFont.ows_dynamicTypeBody
2018-06-11 20:24:29 +02:00
}
}