From ffea3a020f17c0ca73eaf2cd742636755fdbce22 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 7 Jun 2018 19:51:42 -0600 Subject: [PATCH] WIP: FTS - wired up Search VC -[] Backend -[] indexes e5.25 -[x] wire up results: Contacts / Conversations / Messages actual: 3hr -[ ] group thread est: actual: -[x] group name actual: e.25 -[ ] group member name: e.25 -[ ] group member number: e.25 -[ ] contact thread e.5 -[ ] name -[ ] number -[ ] messages e1 -[ ] content -[] Frontend e10.75 -[x] wire up VC's a.5 -[x] show search results only when search box has content a.25 -[] show search results: Contact / Conversation / Messages e2 -[] tapping thread search result takes you to conversation e1 -[] tapping message search result takes you to message e1 -[] show snippet text for matched message e1 -[] highlight matched text in thread e3 -[] go to next search result in thread e2 --- Signal.xcodeproj/project.pbxproj | 4 + .../ConversationSearchViewController.swift | 148 ++++++++++++++++++ .../HomeView/HomeViewController.m | 31 +++- .../translations/en.lproj/Localizable.strings | 3 + SignalMessaging/categories/String+OWS.swift | 6 +- .../utils/ConversationSearcher.swift | 14 +- 6 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b6a979eba..4dcc26b51 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -412,6 +412,7 @@ 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; }; 4AC4EA13C8A444455DAB351F /* Pods_SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */; }; 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; }; + 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; @@ -1065,6 +1066,7 @@ 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = ""; }; 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = ""; }; 45FDA43420A4D22700396358 /* OWSNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSNavigationBar.swift; sourceTree = ""; }; + 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1408,6 +1410,7 @@ 34386A50207D0C01009F5D9C /* HomeViewCell.m */, 34386A4F207D0C01009F5D9C /* HomeViewController.h */, 34386A4D207D0C01009F5D9C /* HomeViewController.m */, + 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */, ); path = HomeView; sourceTree = ""; @@ -3214,6 +3217,7 @@ 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */, 340FC8BA204DAC8D007AEB0F /* FingerprintViewScanController.m in Sources */, 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */, + 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */, 450D19131F85236600970622 /* RemoteVideoView.m in Sources */, B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift new file mode 100644 index 000000000..6e672b09c --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -0,0 +1,148 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +class ConversationSearchViewController: UITableViewController { + + var searchResults: ConversationSearchResults = ConversationSearchResults.empty() + + enum SearchSection: Int { + case conversations = 0 + case contacts = 1 + case messages = 2 + } + + // MARK: View Lifecyle + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.isHidden = true + self.view.backgroundColor = UIColor.yellow + } + + // MARK: UITableViewDelegate + + override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let searchSection = SearchSection(rawValue: section) else { + owsFail("unknown section: \(section)") + return 0 + } + + switch searchSection { + case .conversations: + return searchResults.conversations.count + case .contacts: + return searchResults.contacts.count + case .messages: + return searchResults.messages.count + } + } + + /* + + // Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier: + // Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls) + + @available(iOS 2.0, *) + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell + + + @available(iOS 2.0, *) + optional public func numberOfSections(in tableView: UITableView) -> Int // Default is 1 if not implemented + + + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? // fixed font style. use custom view (UILabel) if you want something different + + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? + + + // Editing + + // Individual rows can opt out of having the -editing property set for them. If not implemented, all rows are assumed to be editable. + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool + + + // Moving/reordering + + // Allows the reorder accessory view to optionally be shown for a particular row. By default, the reorder control will be shown only if the datasource implements -tableView:moveRowAtIndexPath:toIndexPath: + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool + + + // Index + + @available(iOS 2.0, *) + optional public func sectionIndexTitles(for tableView: UITableView) -> [String]? // return list of section titles to display in section index view (e.g. "ABCD...Z#") + + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int // tell table which section corresponds to section title/index (e.g. "B",1)) + + + // Data manipulation - insert and delete support + + // After a row has the minus or plus button invoked (based on the UITableViewCellEditingStyle for the cell), the dataSource must commit the change + // Not called for edit actions using UITableViewRowAction - the action's handler will be invoked instead + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) + + + // Data manipulation - reorder / moving support + + @available(iOS 2.0, *) + optional public func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) + + */ + +} + +extension ConversationSearchViewController: UISearchBarDelegate { +// @available(iOS 2.0, *) +// optional public func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool // return NO to not become first responder +// +// @available(iOS 2.0, *) +// optional public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) // called when text starts editing +// +// @available(iOS 2.0, *) +// optional public func searchBarShouldEndEditing(_ searchBar: UISearchBar) -> Bool // return NO to not resign first responder +// +// @available(iOS 2.0, *) +// optional public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) // called when text ends editing +// + + public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + guard searchText.stripped.count > 0 else { + self.view.isHidden = true + return + } + + self.view.isHidden = false + } + +// +// @available(iOS 3.0, *) +// optional public func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool // called before text changes +// +// +// @available(iOS 2.0, *) +// optional public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) // called when keyboard search button pressed +// +// @available(iOS 2.0, *) +// optional public func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) // called when bookmark button pressed +// +// @available(iOS 2.0, *) +// optional public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) // called when cancel button pressed +// +// @available(iOS 3.2, *) +// optional public func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) // called when search results button pressed +// +// +// @available(iOS 3.0, *) +// optional public func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) +} diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 27a72a5ef..46338fe88 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -54,6 +54,11 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations @property (nonatomic) BOOL shouldObserveDBModifications; @property (nonatomic) BOOL hasBeenPresented; +// Mark: Search + +@property (nonatomic) UISearchBar *searchBar; +@property (nonatomic) ConversationSearchViewController *searchController; + // Dependencies @property (nonatomic, readonly) AccountManager *accountManager; @@ -238,7 +243,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations action:@selector(pullToRefreshPerformed:) forControlEvents:UIControlEventValueChanged]; [self.tableView insertSubview:pullToRefreshView atIndex:0]; - + [self updateReminderViews]; } @@ -261,7 +266,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [super viewDidLoad]; self.editingDbConnection = OWSPrimaryStorage.sharedManager.newDatabaseConnection; - + // Create the database connection. [self uiDatabaseConnection]; @@ -290,6 +295,28 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self registerForPreviewingWithDelegate:self sourceView:self.tableView]; } + // Search + + UISearchBar *searchBar = [UISearchBar new]; + _searchBar = searchBar; + searchBar.searchBarStyle = UISearchBarStyleMinimal; + searchBar.placeholder = NSLocalizedString(@"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER", @"Placeholder text for search bar which filters conversations."); + searchBar.backgroundColor = [UIColor whiteColor]; + [searchBar sizeToFit]; + + // Setting tableHeader calls numberOfSections, which must happen after updateMappings has been called at least once. + ConversationSearchViewController *searchController = [ConversationSearchViewController new]; + self.searchController = searchController; + [self addChildViewController:searchController]; + searchController.view.frame = self.view.frame; + [self.view addSubview:searchController.view]; + // TODO - better/more flexible way to pin below search bar? + [searchController.view autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(58, 0, 0, 0)]; + searchBar.delegate = searchController; + + OWSAssert(self.tableView.tableHeaderView == nil); + self.tableView.tableHeaderView = self.searchBar; + [self updateBarButtonItems]; } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 785fb8d3b..32ff71989 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"; diff --git a/SignalMessaging/categories/String+OWS.swift b/SignalMessaging/categories/String+OWS.swift index 275a318e1..1b56f959b 100644 --- a/SignalMessaging/categories/String+OWS.swift +++ b/SignalMessaging/categories/String+OWS.swift @@ -6,8 +6,12 @@ import Foundation public extension String { + var stripped: String { + return self.trimmingCharacters(in: .whitespacesAndNewlines) + } + // Truncates string to be less than or equal to byteCount, while ensuring we never truncate partial characters for multibyte characters. - public func truncated(toByteCount byteCount: UInt) -> String? { + func truncated(toByteCount byteCount: UInt) -> String? { var lowerBoundCharCount = 0 var upperBoundCharCount = self.count diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index ea7219f9e..2f0b16401 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -15,17 +15,20 @@ public class ConversationSearchItem: NSObject { } } -@objc -public class ConversationSearchResults: NSObject { - let conversations: [ConversationSearchItem] - let contacts: [ConversationSearchItem] - let messages: [ConversationSearchItem] +public class ConversationSearchResults { + public let conversations: [ConversationSearchItem] + public let contacts: [ConversationSearchItem] + public let messages: [ConversationSearchItem] public init(conversations: [ConversationSearchItem], contacts: [ConversationSearchItem], messages: [ConversationSearchItem]) { self.conversations = conversations self.contacts = contacts self.messages = messages } + + public class func empty() -> ConversationSearchResults { + return ConversationSearchResults(conversations: [], contacts: [], messages: []) + } } @objc @@ -40,7 +43,6 @@ public class ConversationSearcher: NSObject { super.init() } - @objc public func results(searchText: String, transaction: YapDatabaseReadTransaction) -> ConversationSearchResults { // TODO limit results, prioritize conversations, then contacts, then messages.