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:
parent
ffea3a020f
commit
a9e2834d9f
|
@ -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()
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -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.";
|
||||
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue