WIP: FTS - rudimentary show results

-[] 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
   -[x] wire up matchs
   -[] style contact cell
   -[] style conversation cell
   -[] style messages cell
  -[] 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
  -[] No Results page
  -[] Hide search unless pulled down
This commit is contained in:
Michael Kirk 2018-06-07 22:00:49 -06:00
parent ffea3a020f
commit a9e2834d9f
6 changed files with 213 additions and 68 deletions

View File

@ -7,7 +7,16 @@ import Foundation
@objc
class ConversationSearchViewController: UITableViewController {
var searchResults: ConversationSearchResults = ConversationSearchResults.empty()
var searchResults: ConversationSearchResults = ConversationSearchResults.empty
var uiDatabaseConnection: YapDatabaseConnection {
// TODO do we want to respond to YapDBModified? Might be hard when there's lots of search results, for only marginal value
return OWSPrimaryStorage.shared().uiDatabaseConnection
}
var searcher: ConversationSearcher {
return ConversationSearcher.shared
}
enum SearchSection: Int {
case conversations = 0
@ -21,12 +30,13 @@ class ConversationSearchViewController: UITableViewController {
super.viewDidLoad()
self.view.isHidden = true
self.view.backgroundColor = UIColor.yellow
self.tableView.register(ChatSearchResultCell.self, forCellReuseIdentifier: ChatSearchResultCell.reuseIdentifier)
}
// MARK: UITableViewDelegate
override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let searchSection = SearchSection(rawValue: section) else {
owsFail("unknown section: \(section)")
return 0
@ -42,22 +52,73 @@ class ConversationSearchViewController: UITableViewController {
}
}
class ChatSearchResultCell: UITableViewCell {
static let reuseIdentifier = "ChatSearchResultCell"
func configure(searchResult: ConversationSearchItem) {
self.textLabel!.text = searchResult.thread.name
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let searchSection = SearchSection(rawValue: indexPath.section) else {
return UITableViewCell()
}
switch searchSection {
case .conversations:
guard let cell = tableView.dequeueReusableCell(withIdentifier: ChatSearchResultCell.reuseIdentifier) as? ChatSearchResultCell else {
return UITableViewCell()
}
guard let searchResult = self.searchResults.conversations[safe: indexPath.row] else {
return UITableViewCell()
}
cell.configure(searchResult: searchResult)
return cell
case .contacts:
// TODO
return UITableViewCell()
case .messages:
// TODO
return UITableViewCell()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
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 .conversations:
if searchResults.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 searchResults.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 searchResults.messages.count > 0 {
return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "section header for search results that match a message in a conversation")
} else {
return nil
}
}
}
/*
// 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?
@ -118,11 +179,18 @@ extension ConversationSearchViewController: UISearchBarDelegate {
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard searchText.stripped.count > 0 else {
self.searchResults = ConversationSearchResults.empty
self.view.isHidden = true
return
}
self.view.isHidden = false
self.uiDatabaseConnection.read { transaction in
self.searchResults = self.searcher.results(searchText: searchText, transaction: transaction)
}
// TODO: more perfomant way to do...
self.tableView.reloadData()
}
//

View File

@ -1753,6 +1753,15 @@
/* No comment provided by engineer. */
"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT" = "Search by name or number";
/* section header for search results that match a contact who doesn't have an existing conversation */
"SEARCH_SECTION_CONTACTS" = "Other Contacts";
/* section header for search results that match existing conversations (either group or contact conversations) */
"SEARCH_SECTION_CONVERSATIONS" = "Conversations";
/* section header for search results that match a message in a conversation */
"SEARCH_SECTION_MESSAGES" = "Messages";
/* No comment provided by engineer. */
"SECURE_SESSION_RESET" = "Secure session was reset.";

View File

@ -26,7 +26,7 @@ public class ConversationSearchResults {
self.messages = messages
}
public class func empty() -> ConversationSearchResults {
public class var empty: ConversationSearchResults {
return ConversationSearchResults(conversations: [], contacts: [], messages: [])
}
}
@ -34,12 +34,12 @@ public class ConversationSearchResults {
@objc
public class ConversationSearcher: NSObject {
private let finder: ConversationFullTextSearchFinder
private let finder: FullTextSearchFinder
@objc
public static let shared: ConversationSearcher = ConversationSearcher()
override private init() {
finder = ConversationFullTextSearchFinder()
finder = FullTextSearchFinder()
super.init()
}
@ -140,49 +140,49 @@ public class ConversationSearcher: NSObject {
}
}
public class ConversationFullTextSearchFinder {
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any) -> Void) {
guard let ext = ext(transaction: transaction) else {
owsFail("ext was unexpectedly nil")
return
}
ext.enumerateKeysAndObjects(matching: searchText) { (_, _, object, _) in
block(object)
}
}
private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? {
return transaction.ext(ConversationFullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction
}
// MARK: - Extension Registration
static let dbExtensionName: String = "ConversationFullTextSearchFinderExtension1"
public class func asyncRegisterDatabaseExtension(storage: OWSStorage) {
storage.asyncRegister(dbExtensionConfig, withName: dbExtensionName)
}
// Only for testing.
public class func syncRegisterDatabaseExtension(storage: OWSStorage) {
storage.register(dbExtensionConfig, withName: dbExtensionName)
}
private class var dbExtensionConfig: YapDatabaseFullTextSearch {
let contentColumnName = "content"
let handler = YapDatabaseFullTextSearchHandler.withObjectBlock { (dict: NSMutableDictionary, _: String, _: String, object: Any) in
if let groupThread = object as? TSGroupThread {
dict[contentColumnName] = groupThread.groupModel.groupName
}
}
// update search index on contact name changes?
// update search index on message insertion?
// TODO is it worth doing faceted search, i.e. Author / Name / Content?
// seems unlikely that mobile users would use the "author: Alice" search syntax.
return YapDatabaseFullTextSearch(columnNames: ["content"], handler: handler)
}
}
//public class ConversationFullTextSearchFinder {
//
// public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any) -> Void) {
// guard let ext = ext(transaction: transaction) else {
// owsFail("ext was unexpectedly nil")
// return
// }
//
// ext.enumerateKeysAndObjects(matching: searchText) { (_, _, object, _) in
// block(object)
// }
// }
//
// private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? {
// return transaction.ext(ConversationFullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction
// }
//
// // MARK: - Extension Registration
//
// static let dbExtensionName: String = "ConversationFullTextSearchFinderExtension1"
//
// public class func asyncRegisterDatabaseExtension(storage: OWSStorage) {
// storage.asyncRegister(dbExtensionConfig, withName: dbExtensionName)
// }
//
// // Only for testing.
// public class func syncRegisterDatabaseExtension(storage: OWSStorage) {
// storage.register(dbExtensionConfig, withName: dbExtensionName)
// }
//
// private class var dbExtensionConfig: YapDatabaseFullTextSearch {
// let contentColumnName = "content"
// let handler = YapDatabaseFullTextSearchHandler.withObjectBlock { (dict: NSMutableDictionary, _: String, _: String, object: Any) in
// if let groupThread = object as? TSGroupThread {
// dict[contentColumnName] = groupThread.groupModel.groupName
// }
// }
//
// // update search index on contact name changes?
// // update search index on message insertion?
//
// // TODO is it worth doing faceted search, i.e. Author / Name / Content?
// // seems unlikely that mobile users would use the "author: Alice" search syntax.
// return YapDatabaseFullTextSearch(columnNames: ["content"], handler: handler)
// }
//}

View File

@ -0,0 +1,54 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class FullTextSearchFinder: NSObject {
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any) -> Void) {
guard let ext = ext(transaction: transaction) else {
assertionFailure("ext was unexpectedly nil")
return
}
ext.enumerateKeysAndObjects(matching: searchText) { (_, _, object, _) in
block(object)
}
}
private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? {
return transaction.ext(FullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction
}
// MARK: - Extension Registration
private static let dbExtensionName: String = "FullTextSearchFinderExtension"
@objc
public class func asyncRegisterDatabaseExtension(storage: OWSStorage) {
storage.asyncRegister(dbExtensionConfig, withName: dbExtensionName)
}
// Only for testing.
public class func syncRegisterDatabaseExtension(storage: OWSStorage) {
storage.register(dbExtensionConfig, withName: dbExtensionName)
}
private class var dbExtensionConfig: YapDatabaseFullTextSearch {
let contentColumnName = "content"
let handler = YapDatabaseFullTextSearchHandler.withObjectBlock { (dict: NSMutableDictionary, _: String, _: String, object: Any) in
if let groupThread = object as? TSGroupThread {
dict[contentColumnName] = groupThread.groupModel.groupName
}
}
// update search index on contact name changes?
// update search index on message insertion?
// TODO is it worth doing faceted search, i.e. Author / Name / Content?
// seems unlikely that mobile users would use the "author: Alice" search syntax.
return YapDatabaseFullTextSearch(columnNames: ["content"], handler: handler)
}
}

View File

@ -16,6 +16,7 @@
#import "OWSStorage+Subclass.h"
#import "TSDatabaseSecondaryIndexes.h"
#import "TSDatabaseView.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -58,13 +59,15 @@ void RunAsyncRegistrationsForStorage(OWSStorage *storage, dispatch_block_t compl
[TSDatabaseView asyncRegisterThreadOutgoingMessagesDatabaseView:storage];
[TSDatabaseView asyncRegisterThreadSpecialMessagesDatabaseView:storage];
// Register extensions which aren't essential for rendering threads async.
[FullTextSearchFinder asyncRegisterDatabaseExtensionWithStorage:storage];
[OWSIncomingMessageFinder asyncRegisterExtensionWithPrimaryStorage:storage];
[TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView:storage];
[OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:storage];
[OWSFailedMessagesJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
[OWSFailedAttachmentDownloadsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
[OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
// NOTE: Always pass the completion to the _LAST_ of the async database
// view registrations.
[TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:storage completion:completion];

View File

@ -18,6 +18,8 @@
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseCrossProcessNotification.h>
#import <YapDatabase/YapDatabaseCryptoUtils.h>
#import <YapDatabase/YapDatabaseFullTextSearch.h>
#import <YapDatabase/YapDatabaseFullTextSearchPrivate.h>
#import <YapDatabase/YapDatabaseSecondaryIndex.h>
#import <YapDatabase/YapDatabaseSecondaryIndexPrivate.h>
@ -536,6 +538,15 @@ NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_
extensionName:extensionName]
options:secondaryIndex->options];
return secondaryIndexCopy;
} else if ([extension isKindOfClass:[YapDatabaseFullTextSearch class]]) {
YapDatabaseFullTextSearch *fullTextSearch = (YapDatabaseFullTextSearch *)extension;
NSString *versionTag = [self appendSuffixToDatabaseExtensionVersionIfNecessary:fullTextSearch.versionTag extensionName:extensionName];
YapDatabaseFullTextSearch *fullTextSearchCopy = [[YapDatabaseFullTextSearch alloc] initWithColumnNames:fullTextSearch->columnNames.array
handler:fullTextSearch->handler
versionTag:versionTag];
return fullTextSearchCopy;
} else if ([extension isKindOfClass:[YapDatabaseCrossProcessNotification class]]) {
// versionTag doesn't matter for YapDatabaseCrossProcessNotification.
return extension;