From 10abbb05527144eed253bd1af373095ff990b4f0 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Mon, 17 Jan 2022 16:56:51 +1100 Subject: [PATCH] search result view UI --- Session/Home/GlobalSearch.swift | 104 +++++++++++++++--- Session/Home/HomeVC+Search.swift | 22 +++- Session/Home/HomeVC.swift | 26 ++++- .../Translations/en.lproj/Localizable.strings | 2 + Session/Shared/ConversationCell.swift | 28 ++++- 5 files changed, 164 insertions(+), 18 deletions(-) diff --git a/Session/Home/GlobalSearch.swift b/Session/Home/GlobalSearch.swift index 0c005e58a..811e4d1cd 100644 --- a/Session/Home/GlobalSearch.swift +++ b/Session/Home/GlobalSearch.swift @@ -1,12 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import UIKit @objc public protocol GlobalSearchViewDelegate: AnyObject { func globalSearchViewWillBeginDragging() - - func globalSearchDidSelectSearchResult(thread: ThreadViewModel, messageId: String?) } @objc @@ -123,7 +122,7 @@ public class GlobalSearchViewController: UITableViewController { var searchResults: HomeScreenSearchResultSet? self.dbReadConnection.asyncRead({[weak self] transaction in guard let strongSelf = self else { return } - searchResults = strongSelf.searcher.searchForHomeScreen(searchText: searchText, transaction: transaction) + searchResults = strongSelf.searcher.searchForHomeScreen(searchText: searchText, transaction: transaction) }, completionBlock: { [weak self] in AssertIsOnMainThread() guard let self = self else { return } @@ -151,24 +150,92 @@ extension GlobalSearchViewController { public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) - guard let searchSection = SearchSection(rawValue: indexPath.section) else { return } - switch searchSection { case .noResults: SNLog("shouldn't be able to tap 'no results' section") case .contacts: let sectionResults = searchResultSet.conversations - guard let searchResult = sectionResults[safe: indexPath.row] else { return } - delegate?.globalSearchDidSelectSearchResult(thread: searchResult.thread, messageId: searchResult.messageId) + guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return } + show(thread, highlightedMessageID: nil, animated: true) case .messages: let sectionResults = searchResultSet.messages - guard let searchResult = sectionResults[safe: indexPath.row] else { return } - delegate?.globalSearchDidSelectSearchResult(thread: searchResult.thread, messageId: searchResult.messageId) + guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return } + show(thread, highlightedMessageID: nil, animated: true) + } + tableView.deselectRow(at: indexPath, animated: true) + } + + private func show(_ thread: TSThread, highlightedMessageID: String?, animated: Bool) { + DispatchMainThreadSafe { + if let presentedVC = self.presentedViewController { + presentedVC.dismiss(animated: false, completion: nil) + } + let conversationVC = ConversationVC(thread: thread, focusedMessageID: highlightedMessageID) + self.navigationController?.pushViewController(conversationVC, animated: true) } } // MARK: UITableViewDataSource + + public override func numberOfSections(in tableView: UITableView) -> Int { + return 3 + } + + public override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + UIView() + } + + public override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + .leastNonzeroMagnitude + } + + public override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard nil != self.tableView(tableView, titleForHeaderInSection: section) else { + return .leastNonzeroMagnitude + } + return UITableView.automaticDimension + } + + public override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let title = self.tableView(tableView, titleForHeaderInSection: section) else { + return UIView() + } + + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize) + + let container = UIView() + container.backgroundColor = Colors.cellBackground + container.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, left: Values.mediumSpacing, bottom: Values.smallSpacing, right: Values.mediumSpacing) + container.addSubview(titleLabel) + titleLabel.autoPinEdgesToSuperviewMargins() + + return container + } + + public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard let searchSection = SearchSection(rawValue: section) else { return nil } + + switch searchSection { + case .noResults: + return nil + case .contacts: + if searchResultSet.conversations.count > 0 { + return NSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "") + } else { + return nil + } + case .messages: + if searchResultSet.messages.count > 0 { + return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "") + } else { + return nil + } + } + } public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let searchSection = SearchSection(rawValue: section) else { return 0 } @@ -197,10 +264,21 @@ extension GlobalSearchViewController { guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell, indexPath.row == 0 else { return UITableViewCell() } cell.configure(searchText: searchText) return cell - case .contacts, .messages: - // TODO: return correct cell - guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell else { return UITableViewCell() } - cell.configure(searchText: searchText) + case .contacts: + let sectionResults = searchResultSet.conversations + let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell + cell.isShowingGlobalSearchResult = true + let searchResult = sectionResults[safe: indexPath.row] + cell.threadViewModel = searchResult?.thread + cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet) + return cell + case .messages: + let sectionResults = searchResultSet.messages + let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell + cell.isShowingGlobalSearchResult = true + let searchResult = sectionResults[safe: indexPath.row] + cell.threadViewModel = searchResult?.thread + cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet) return cell } } diff --git a/Session/Home/HomeVC+Search.swift b/Session/Home/HomeVC+Search.swift index 53c763d7b..829a6e7b3 100644 --- a/Session/Home/HomeVC+Search.swift +++ b/Session/Home/HomeVC+Search.swift @@ -2,13 +2,23 @@ import UIKit extension HomeVC: UISearchBarDelegate, GlobalSearchViewDelegate { - func GlobalSearchViewWillBeginDragging() { + func globalSearchViewWillBeginDragging() { } // MARK: UISearchBarDelegate func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + self.updateSearchResultsVisibility() + self.ensureSearchBarCancelButton() + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + self.updateSearchResultsVisibility() + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + self.updateSearchResultsVisibility() self.ensureSearchBarCancelButton() } @@ -19,8 +29,16 @@ extension HomeVC: UISearchBarDelegate, GlobalSearchViewDelegate { } func ensureSearchBarCancelButton() { - let shouldShowCancelButton = searchBar.isFirstResponder + let shouldShowCancelButton = searchBar.isFirstResponder || (searchBar.text ?? "").count > 0 guard searchBar.showsCancelButton != shouldShowCancelButton else { return } self.searchBar.setShowsCancelButton(shouldShowCancelButton, animated: true) } + + func updateSearchResultsVisibility() { + guard let searchText = searchBar.text?.ows_stripped() else { return } + searchResultsController.searchText = searchText + let isSearching = searchText.count > 0 + searchResultsController.view.isHidden = !isSearching + tableView.isScrollEnabled = !isSearching + } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 1d720ff13..2fd5121c9 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -37,13 +37,13 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv return result }() - internal lazy var searchController: GlobalSearchViewController = { + internal lazy var searchResultsController: GlobalSearchViewController = { let result = GlobalSearchViewController() result.delegate = self return result }() - private lazy var tableView: UITableView = { + internal lazy var tableView: UITableView = { let result = UITableView() result.backgroundColor = .clear result.separatorStyle = .none @@ -178,13 +178,35 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv searchBarContainer.addSubview(searchBar) searchBar.autoPinEdgesToSuperviewMargins() tableView.tableHeaderView = searchBarContainer + + addChild(searchResultsController) + view.addSubview(searchResultsController.view) + searchResultsController.view.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + searchResultsController.view.autoPinEdge(toSuperviewEdge: .top, withInset: 64) + searchResultsController.view.isHidden = true } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + searchResultsController.viewDidAppear(animated) reload() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + searchResultsController.viewWillAppear(animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + searchResultsController.viewWillDisappear(animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + searchResultsController.viewDidDisappear(animated) + } + deinit { NotificationCenter.default.removeObserver(self) } diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index a58f90996..32f3a032b 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -579,3 +579,5 @@ "light_mode_theme" = "Light"; "PIN_BUTTON_TEXT" = "Pin"; "UNPIN_BUTTON_TEXT" = "Unpin"; +"SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; +"SEARCH_SECTION_MESSAGES" = "Messages"; diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 2a7edf38a..35164e8aa 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -2,7 +2,12 @@ import UIKit import SessionUIKit final class ConversationCell : UITableViewCell { - var threadViewModel: ThreadViewModel! { didSet { update() } } + var isShowingGlobalSearchResult = false + var threadViewModel: ThreadViewModel! { + didSet { + isShowingGlobalSearchResult ? updateForSearchResult() : update() + } + } static let reuseIdentifier = "ConversationCell" @@ -189,6 +194,27 @@ final class ConversationCell : UITableViewCell { stackView.set(.height, to: cellHeight) } + // MARK: Updating for search results + private func updateForSearchResult() { + AssertIsOnMainThread() + guard let thread = threadViewModel?.threadRecord else { return } + profilePictureView.update(for: thread) + displayNameLabel.text = getDisplayName() + isPinnedIcon.isHidden = true + unreadCountView.isHidden = true + hasMentionView.isHidden = true + } + + public func configure(messageDate: Date?, snippet: String?) { + if let messageDate = messageDate, let snippet = snippet { + timestampLabel.text = DateUtil.formatDate(forDisplay: messageDate) + snippetLabel.text = snippet + } else { + timestampLabel.text = DateUtil.formatDate(forDisplay: threadViewModel.lastMessageDate) + snippetLabel.attributedText = getSnippet() + } + } + // MARK: Updating private func update() { AssertIsOnMainThread()