WIP: message indexing

pull/1/head
Michael Kirk 6 years ago
parent b00e5a4fd9
commit 3a03c4f74c

@ -32,6 +32,7 @@ class ConversationSearchViewController: UITableViewController {
self.view.isHidden = true
self.tableView.register(ChatSearchResultCell.self, forCellReuseIdentifier: ChatSearchResultCell.reuseIdentifier)
self.tableView.register(MessageSearchResultCell.self, forCellReuseIdentifier: MessageSearchResultCell.reuseIdentifier)
}
// MARK: UITableViewDelegate
@ -55,8 +56,93 @@ class ConversationSearchViewController: UITableViewController {
class ChatSearchResultCell: UITableViewCell {
static let reuseIdentifier = "ChatSearchResultCell"
let nameLabel: UILabel
let snippetLabel: UILabel
let avatarView: AvatarImageView
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.nameLabel = UILabel()
self.snippetLabel = UILabel()
self.avatarView = AvatarImageView()
avatarView.autoSetDimensions(to: CGSize(width: 40, height: 40))
super.init(style: style, reuseIdentifier: reuseIdentifier)
let textRows = UIStackView(arrangedSubviews: [nameLabel, snippetLabel])
textRows.axis = .vertical
let columns = UIStackView(arrangedSubviews: [avatarView, textRows])
columns.axis = .horizontal
columns.spacing = 8
contentView.addSubview(columns)
columns.autoPinEdgesToSuperviewMargins()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(searchResult: SearchResult) {
self.textLabel!.text = searchResult.thread.name
self.nameLabel.text = searchResult.thread.name
self.snippetLabel.text = searchResult.snippet
}
}
class MessageSearchResultCell: UITableViewCell {
static let reuseIdentifier = "MessageSearchResultCell"
let nameLabel: UILabel
let snippetLabel: UILabel
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.nameLabel = UILabel()
self.snippetLabel = UILabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
let textRows = UIStackView(arrangedSubviews: [nameLabel, snippetLabel])
textRows.axis = .vertical
contentView.addSubview(textRows)
textRows.autoPinEdgesToSuperviewMargins()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(searchResult: SearchResult) {
self.nameLabel.text = searchResult.thread.name
// [[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding]
// options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
// NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
// documentAttributes:nil error:nil];
guard let snippet = searchResult.snippet else {
self.snippetLabel.text = nil
return
}
guard let encodedString = snippet.data(using: .utf8) else {
self.snippetLabel.text = nil
return
}
// NSAttributedString(data: <#T##Data#>, options: [, ], documentAttributes: <#T##AutoreleasingUnsafeMutablePointer<NSDictionary?>?#>)
// Bold snippet text
do {
// TODO NSAttributedString.DocumentReadingOptionKey.characterEncoding: .utf8
let attributedSnippet = try NSAttributedString(data: encodedString,
options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil)
self.snippetLabel.attributedText = attributedSnippet
} catch {
owsFail("failed to generate snippet: \(error)")
}
}
}
@ -79,10 +165,18 @@ class ConversationSearchViewController: UITableViewController {
return cell
case .contacts:
// TODO
return UITableViewCell()
return UITableViewCell()
case .messages:
// TODO
guard let cell = tableView.dequeueReusableCell(withIdentifier: MessageSearchResultCell.reuseIdentifier) as? MessageSearchResultCell else {
return UITableViewCell()
}
guard let searchResult = self.searchResultSet.messages[safe: indexPath.row] else {
return UITableViewCell()
}
cell.configure(searchResult: searchResult)
return cell
}
}

@ -26,6 +26,7 @@ class ConversationSearcherTest: XCTestCase {
TSContactThread.removeAllObjectsInCollection()
TSGroupThread.removeAllObjectsInCollection()
TSMessage.removeAllObjectsInCollection()
self.dbConnection.readWrite { transaction in
let bookModel = TSGroupModel(title: "Book Club", memberIds: [], image: nil, groupId: Randomness.generateRandomBytes(16))
@ -41,6 +42,18 @@ class ConversationSearcherTest: XCTestCase {
let bobContactThread = TSContactThread.getOrCreateThread(withContactId: "+49030183000", transaction: transaction)
self.bobThread = ThreadViewModel(thread: bobContactThread, transaction: transaction)
let helloAlice = TSOutgoingMessage(in: aliceContactThread, messageBody: "Hello Alice", attachmentId: nil)
helloAlice.save(with: transaction)
let goodbyeAlice = TSOutgoingMessage(in: aliceContactThread, messageBody: "Goodbye Alice", attachmentId: nil)
goodbyeAlice.save(with: transaction)
let helloBookClub = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "Hello Book Club", attachmentId: nil)
helloBookClub.save(with: transaction)
let goodbyeBookClub = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "Goodbye Book Club", attachmentId: nil)
goodbyeBookClub.save(with: transaction)
}
}
@ -144,7 +157,7 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual(aliceThread, resultSet.conversations.first?.thread)
}
func testSearchContactByName() {
func pending_testSearchConversationByContactByName() {
var resultSet: SearchResultSet = .empty
resultSet = getResultSet(searchText: "Alice")
@ -160,6 +173,19 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual(bobThread, resultSet.conversations.first?.thread)
}
func testSearchMessageByBodyContent() {
var resultSet: SearchResultSet = .empty
resultSet = getResultSet(searchText: "Hello Alice")
XCTAssertEqual(1, resultSet.messages.count)
XCTAssertEqual(aliceThread, resultSet.messages.first?.thread)
resultSet = getResultSet(searchText: "Hello")
XCTAssertEqual(2, resultSet.messages.count)
XCTAssert(resultSet.messages.map { $0.thread }.contains(aliceThread))
XCTAssert(resultSet.messages.map { $0.thread }.contains(bookClubThread))
}
// Mark: Helpers
private func getResultSet(searchText: String) -> SearchResultSet {

@ -5,13 +5,13 @@
import Foundation
import SignalServiceKit
@objc
public class SearchResult: NSObject {
@objc
public class SearchResult {
public let thread: ThreadViewModel
public let snippet: String?
init(thread: ThreadViewModel) {
init(thread: ThreadViewModel, snippet: String?) {
self.thread = thread
self.snippet = snippet
}
}
@ -50,12 +50,20 @@ public class ConversationSearcher: NSObject {
var contacts: [SearchResult] = []
var messages: [SearchResult] = []
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any) in
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
if let thread = match as? TSThread {
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
let searchResult = SearchResult(thread: threadViewModel)
let snippet: String? = thread.lastMessageText(transaction: transaction)
let searchResult = SearchResult(thread: threadViewModel, snippet: snippet)
conversations.append(searchResult)
} else if let message = match as? TSMessage {
let thread = message.thread(with: transaction)
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
let searchResult = SearchResult(thread: threadViewModel, snippet: snippet)
messages.append(searchResult)
} else {
Logger.debug("\(self.logTag) in \(#function) unhandled item: \(match)")
}

@ -21,8 +21,8 @@ public class SearchIndexer<T> {
@objc
public class FullTextSearchFinder: NSObject {
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any) -> Void) {
guard let ext = ext(transaction: transaction) else {
public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) {
guard let ext: YapDatabaseFullTextSearchTransaction = ext(transaction: transaction) else {
assertionFailure("ext was unexpectedly nil")
return
}
@ -33,8 +33,9 @@ public class FullTextSearchFinder: NSObject {
// TODO a stricter "whole word" query for body text?
let prefixQuery = "*\(normalized)*"
ext.enumerateKeysAndObjects(matching: prefixQuery) { (_, _, object, _) in
block(object)
// (snippet: String, collection: String, key: String, object: Any, stop: UnsafeMutablePointer<ObjCBool>)
ext.enumerateKeysAndObjects(matching: prefixQuery, with: nil) { (snippet: String, _: String, _: String, object: Any, _: UnsafeMutablePointer<ObjCBool>) in
block(object, snippet)
}
}
@ -82,11 +83,20 @@ public class FullTextSearchFinder: NSObject {
return normalize(text: searchableContent)
}
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage) in
let searchableContent = message.body ?? ""
return normalize(text: searchableContent)
}
private class func indexContent(object: Any) -> String? {
if let groupThread = object as? TSGroupThread {
return self.groupThreadIndexer.index(groupThread)
} else if let contactThread = object as? TSContactThread {
return self.contactThreadIndexer.index(contactThread)
} else if let message = object as? TSMessage {
return self.messageIndexer.index(message)
} else {
return nil
}

Loading…
Cancel
Save