WIP: global search UI

pull/555/head
ryanzhao 3 years ago
parent 9ec749285f
commit fed1218538

@ -140,6 +140,8 @@
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
7BA9057C278E58B300998B3C /* HomeVC+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA9057B278E58B300998B3C /* HomeVC+Search.swift */; };
7BA9057E27911C5800998B3C /* GlobalSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA9057D27911C5800998B3C /* GlobalSearch.swift */; };
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; };
@ -1107,6 +1109,8 @@
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
7BA9057B278E58B300998B3C /* HomeVC+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeVC+Search.swift"; sourceTree = "<group>"; };
7BA9057D27911C5800998B3C /* GlobalSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearch.swift; sourceTree = "<group>"; };
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -2756,7 +2760,9 @@
isa = PBXGroup;
children = (
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
7BA9057B278E58B300998B3C /* HomeVC+Search.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
7BA9057D27911C5800998B3C /* GlobalSearch.swift */,
);
path = Home;
sourceTree = "<group>";
@ -4777,6 +4783,7 @@
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
7BA9057C278E58B300998B3C /* HomeVC+Search.swift in Sources */,
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */,
@ -4811,6 +4818,7 @@
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */,
7BA9057E27911C5800998B3C /* GlobalSearch.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,

@ -538,6 +538,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
func showSearchUI() {
isShowingSearchUI = true
// Search bar
// FIXME: This code is duplicated with SearchBar
let searchBar = searchController.uiSearchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.barStyle = .black

@ -0,0 +1,269 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@objc
public protocol GlobalSearchViewDelegate: AnyObject {
func globalSearchViewWillBeginDragging()
func globalSearchDidSelectSearchResult(thread: ThreadViewModel, messageId: String?)
}
@objc
public class GlobalSearchViewController: UITableViewController {
@objc
public weak var delegate: GlobalSearchViewDelegate?
@objc
public var searchText = "" {
didSet {
AssertIsOnMainThread()
// Use a slight delay to debounce updates.
refreshSearchResults()
}
}
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
private var lastSearchText: String?
var searcher: FullTextSearcher {
return FullTextSearcher.shared
}
enum SearchSection: Int {
case noResults
case contacts
case messages
}
// MARK: Dependencies
var dbReadConnection: YapDatabaseConnection {
return OWSPrimaryStorage.shared().dbReadConnection
}
// MARK: View Lifecycle
init() {
super.init(style: .grouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
tableView.separatorColor = .clear
tableView.separatorInset = .zero
tableView.separatorStyle = .none
tableView.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier)
tableView.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
}
private func reloadTableData() {
tableView.reloadData()
}
// MARK: Update Search Results
var refreshTimer: Timer?
private func refreshSearchResults() {
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
}
if refreshTimer != nil {
// Don't start a new refresh timer if there's already one active.
return
}
refreshTimer?.invalidate()
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.updateSearchResults(searchText: strongSelf.searchText)
strongSelf.refreshTimer = nil
}
}
private func updateSearchResults(searchText rawSearchText: String) {
let searchText = rawSearchText.stripped
guard searchText.count > 0 else {
searchResultSet = HomeScreenSearchResultSet.empty
lastSearchText = nil
reloadTableData()
return
}
guard lastSearchText != searchText else {
// Ignoring redundant search.
return
}
lastSearchText = searchText
var searchResults: HomeScreenSearchResultSet?
self.dbReadConnection.asyncRead({[weak self] transaction in
guard let strongSelf = self else { return }
searchResults = strongSelf.searcher.searchForHomeScreen(searchText: searchText, transaction: transaction)
}, completionBlock: { [weak self] in
AssertIsOnMainThread()
guard let self = self else { return }
guard let results = searchResults else {
owsFailDebug("searchResults was unexpectedly nil")
return
}
guard self.lastSearchText == searchText else {
// Discard results from stale search.
return
}
self.searchResultSet = results
self.reloadTableData()
})
}
}
// MARK: - UITableView
extension GlobalSearchViewController {
// MARK: UITableViewDelegate
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)
case .messages:
let sectionResults = searchResultSet.messages
guard let searchResult = sectionResults[safe: indexPath.row] else { return }
delegate?.globalSearchDidSelectSearchResult(thread: searchResult.thread, messageId: searchResult.messageId)
}
}
// MARK: UITableViewDataSource
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let searchSection = SearchSection(rawValue: section) else { return 0 }
switch searchSection {
case .noResults:
return searchResultSet.isEmpty ? 1 : 0
case .contacts:
return searchResultSet.conversations.count
case .messages:
return searchResultSet.messages.count
}
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
return UITableViewCell()
}
switch searchSection {
case .noResults:
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)
return cell
}
}
}
// MARK: - UIScrollViewDelegate
extension GlobalSearchViewController {
public override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
delegate?.globalSearchViewWillBeginDragging()
}
}
// MARK: -
class EmptySearchResultCell: UITableViewCell {
static let reuseIdentifier = "EmptySearchResultCell"
let messageLabel: UILabel
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
override init(style: UITableViewCell.CellStyle, 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()
contentView.addSubview(activityIndicator)
activityIndicator.autoCenterInSuperview()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
public func configure(searchText: String) {
if searchText.isEmpty {
activityIndicator.color = Colors.text
activityIndicator.isHidden = false
activityIndicator.startAnimating()
messageLabel.isHidden = true
messageLabel.text = nil
} else {
activityIndicator.stopAnimating()
activityIndicator.isHidden = true
messageLabel.isHidden = false
messageLabel.text = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "")
messageLabel.textColor = Colors.text
}
}
}

@ -0,0 +1,26 @@
import UIKit
extension HomeVC: UISearchBarDelegate, GlobalSearchViewDelegate {
func GlobalSearchViewWillBeginDragging() {
}
// MARK: UISearchBarDelegate
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
self.ensureSearchBarCancelButton()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = nil
searchBar.resignFirstResponder()
self.ensureSearchBarCancelButton()
}
func ensureSearchBarCancelButton() {
let shouldShowCancelButton = searchBar.isFirstResponder
guard searchBar.showsCancelButton != shouldShowCancelButton else { return }
self.searchBar.setShowsCancelButton(shouldShowCancelButton, animated: true)
}
}

@ -30,6 +30,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
result.delegate = self
return result
}()
internal lazy var searchBar: SearchBar = {
let result = SearchBar()
result.delegate = self
return result
}()
internal lazy var searchController: GlobalSearchViewController = {
let result = GlobalSearchViewController()
result.delegate = self
return result
}()
private lazy var tableView: UITableView = {
let result = UITableView()
@ -157,6 +169,15 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
// Get default open group rooms if needed
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
// Search
let searchBarContainer = UIView()
searchBarContainer.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
searchBar.sizeToFit()
searchBar.layoutMargins = UIEdgeInsets.zero
searchBarContainer.frame = searchBar.frame
searchBarContainer.addSubview(searchBar)
searchBar.autoPinEdgesToSuperviewMargins()
tableView.tableHeaderView = searchBarContainer
}
override func viewDidAppear(_ animated: Bool) {

@ -85,14 +85,14 @@ public class FullTextSearchFinder: NSObject {
return query
}
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
public func enumerateObjects(searchText: String, maxSearchResults: Int? = nil, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
guard let ext: YapDatabaseFullTextSearchTransaction = ext(transaction: transaction) else {
return
}
let query = FullTextSearchFinder.query(searchText: searchText)
let maxSearchResults = 500
let maxSearchResults = maxSearchResults ?? 500
var searchResultCount = 0
let snippetOptions = YapDatabaseFullTextSearchSnippetOptions()
snippetOptions.startMatchText = ""

@ -33,12 +33,5 @@ public final class SearchBar : UISearchBar {
setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: UISearchBar.Icon.search)
searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: UISearchBar.Icon.clear)
searchTextField.removeConstraints(searchTextField.constraints)
searchTextField.pin(.leading, to: .leading, of: searchTextField.superview!, withInset: Values.mediumSpacing + 3)
searchTextField.pin(.top, to: .top, of: searchTextField.superview!, withInset: 10)
searchTextField.superview!.pin(.trailing, to: .trailing, of: searchTextField, withInset: Values.mediumSpacing + 3)
searchTextField.superview!.pin(.bottom, to: .bottom, of: searchTextField, withInset: 10)
searchTextField.set(.height, to: Values.searchBarHeight)
searchTextField.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
}
}

@ -227,6 +227,7 @@ public class FullTextSearcher: NSObject {
}
public func searchForHomeScreen(searchText: String,
maxSearchResults: Int? = nil,
transaction: YapDatabaseReadTransaction) -> HomeScreenSearchResultSet {
var conversations: [ConversationSearchResult<ConversationSortKey>] = []
@ -234,7 +235,7 @@ public class FullTextSearcher: NSObject {
var existingConversationRecipientIds: Set<String> = Set()
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
self.finder.enumerateObjects(searchText: searchText, maxSearchResults: maxSearchResults, transaction: transaction) { (match: Any, snippet: String?) in
if let thread = match as? TSThread {
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)

Loading…
Cancel
Save