Merge branch 'mkirk/fts-sin-country-code'

pull/1/head
Michael Kirk 7 years ago
commit 7bb93e265d

@ -90,6 +90,7 @@ def disable_optimizations_for_tests(installer)
# Allow accurate step-thru debugging while in tests
build_config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'
build_config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
end
end

@ -237,6 +237,6 @@ SPEC CHECKSUMS:
YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
PODFILE CHECKSUM: 6a1bafb7c5bedfa4e577580ff12e487cc7111f38
PODFILE CHECKSUM: 497de356704a8d141a2dd132ab5c7fa4acffe2b6
COCOAPODS: 1.5.3

@ -1 +1 @@
Subproject commit 47c8a2611481201e17387b4335421f373f8c8e9b
Subproject commit 8c7fd79eba19166ed7b3d4b09b11e11ab130e457

@ -7,7 +7,7 @@ import XCTest
@testable import SignalMessaging
@objc
class FakeEnvironment: TextSecureKitEnv {
class StubbableEnvironment: TextSecureKitEnv {
let proxy: TextSecureKitEnv
init(proxy: TextSecureKitEnv) {
@ -17,51 +17,36 @@ class FakeEnvironment: TextSecureKitEnv {
var stubbedCallMessageHandler: OWSCallMessageHandler?
override var callMessageHandler: OWSCallMessageHandler {
if let callMessageHandler = stubbedCallMessageHandler {
return callMessageHandler
}
return proxy.callMessageHandler
return stubbedCallMessageHandler ?? proxy.callMessageHandler
}
var stubbedContactsManager: ContactsManagerProtocol?
override var contactsManager: ContactsManagerProtocol {
if let contactsManager = stubbedContactsManager {
return contactsManager
}
return proxy.contactsManager
return stubbedContactsManager ?? proxy.contactsManager
}
var stubbedMessageSender: MessageSender?
override var messageSender: MessageSender {
if let messageSender = stubbedMessageSender {
return messageSender
}
return proxy.messageSender
return stubbedMessageSender ?? proxy.messageSender
}
var stubbedNotificationsManager: NotificationsProtocol?
override var notificationsManager: NotificationsProtocol {
if let notificationsManager = stubbedNotificationsManager {
return notificationsManager
}
return proxy.notificationsManager
return stubbedNotificationsManager ?? proxy.notificationsManager
}
var stubbedProfileManager: ProfileManagerProtocol?
override var profileManager: ProfileManagerProtocol {
if let profileManager = stubbedProfileManager {
return profileManager
}
return proxy.profileManager
return stubbedProfileManager ?? proxy.profileManager
}
}
@objc
class FakeContactsManager: NSObject, ContactsManagerProtocol {
func displayName(forPhoneIdentifier phoneNumber: String?) -> String {
if phoneNumber == "+12345678900" {
if phoneNumber == aliceRecipientId {
return "Alice"
} else if phoneNumber == "+49030183000" {
} else if phoneNumber == bobRecipientId {
return "Bob Barker"
} else {
return ""
@ -81,6 +66,9 @@ class FakeContactsManager: NSObject, ContactsManagerProtocol {
}
}
let bobRecipientId = "+49030183000"
let aliceRecipientId = "+12345678900"
class ConversationSearcherTest: XCTestCase {
// MARK: - Dependencies
@ -113,24 +101,24 @@ class ConversationSearcherTest: XCTestCase {
originalEnvironment = TextSecureKitEnv.shared()
let testEnvironment: FakeEnvironment = FakeEnvironment(proxy: originalEnvironment!)
let testEnvironment: StubbableEnvironment = StubbableEnvironment(proxy: originalEnvironment!)
testEnvironment.stubbedContactsManager = FakeContactsManager()
TextSecureKitEnv.setShared(testEnvironment)
self.dbConnection.readWrite { transaction in
let bookModel = TSGroupModel(title: "Book Club", memberIds: ["+12345678900", "+49030183000"], image: nil, groupId: Randomness.generateRandomBytes(16))
let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(16))
let bookClubGroupThread = TSGroupThread.getOrCreateThread(with: bookModel, transaction: transaction)
self.bookClubThread = ThreadViewModel(thread: bookClubGroupThread, transaction: transaction)
let snackModel = TSGroupModel(title: "Snack Club", memberIds: ["+12345678900"], image: nil, groupId: Randomness.generateRandomBytes(16))
let snackModel = TSGroupModel(title: "Snack Club", memberIds: [aliceRecipientId], image: nil, groupId: Randomness.generateRandomBytes(16))
let snackClubGroupThread = TSGroupThread.getOrCreateThread(with: snackModel, transaction: transaction)
self.snackClubThread = ThreadViewModel(thread: snackClubGroupThread, transaction: transaction)
let aliceContactThread = TSContactThread.getOrCreateThread(withContactId: "+12345678900", transaction: transaction)
let aliceContactThread = TSContactThread.getOrCreateThread(withContactId: aliceRecipientId, transaction: transaction)
self.aliceThread = ThreadViewModel(thread: aliceContactThread, transaction: transaction)
let bobContactThread = TSContactThread.getOrCreateThread(withContactId: "+49030183000", transaction: transaction)
self.bobThread = ThreadViewModel(thread: bobContactThread, transaction: transaction)
let bobContactThread = TSContactThread.getOrCreateThread(withContactId: bobRecipientId, transaction: transaction)
self.bobEmptyThread = ThreadViewModel(thread: bobContactThread, transaction: transaction)
let helloAlice = TSOutgoingMessage(in: aliceContactThread, messageBody: "Hello Alice", attachmentId: nil)
helloAlice.save(with: transaction)
@ -152,7 +140,7 @@ class ConversationSearcherTest: XCTestCase {
var snackClubThread: ThreadViewModel!
var aliceThread: ThreadViewModel!
var bobThread: ThreadViewModel!
var bobEmptyThread: ThreadViewModel!
// MARK: Tests
@ -191,7 +179,7 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual(0, threads.count)
// Exact match
threads = searchConversations(searchText: "+12345678900")
threads = searchConversations(searchText: aliceRecipientId)
XCTAssertEqual(3, threads.count)
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
@ -206,8 +194,8 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
threads = searchConversations(searchText: "49")
XCTAssertEqual(2, threads.count)
XCTAssertEqual([bookClubThread, bobThread], threads)
XCTAssertEqual(1, threads.count)
XCTAssertEqual([bookClubThread], threads)
threads = searchConversations(searchText: "1-234-56")
XCTAssertEqual(3, threads.count)
@ -222,18 +210,16 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
}
// TODO
func pending_testSearchContactByNumber() {
var resultSet: SearchResultSet = .empty
func testSearchContactByNumberWithoutCountryCode() {
var threads: [ThreadViewModel] = []
// Phone Number formatting should be forgiving
resultSet = getResultSet(searchText: "234.56")
XCTAssertEqual(1, resultSet.conversations.count)
XCTAssertEqual(aliceThread, resultSet.conversations.first?.thread)
threads = searchConversations(searchText: "234.56")
XCTAssertEqual(3, threads.count)
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
resultSet = getResultSet(searchText: "234 56")
XCTAssertEqual(1, resultSet.conversations.count)
XCTAssertEqual(aliceThread, resultSet.conversations.first?.thread)
threads = searchConversations(searchText: "234 56")
XCTAssertEqual(3, threads.count)
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
}
func testSearchConversationByContactByName() {
@ -244,16 +230,16 @@ class ConversationSearcherTest: XCTestCase {
XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads)
threads = searchConversations(searchText: "Bob")
XCTAssertEqual(2, threads.count)
XCTAssertEqual([bookClubThread, bobThread], threads)
XCTAssertEqual(1, threads.count)
XCTAssertEqual([bookClubThread], threads)
threads = searchConversations(searchText: "Barker")
XCTAssertEqual(2, threads.count)
XCTAssertEqual([bookClubThread, bobThread], threads)
XCTAssertEqual(1, threads.count)
XCTAssertEqual([bookClubThread], threads)
threads = searchConversations(searchText: "Bob B")
XCTAssertEqual(2, threads.count)
XCTAssertEqual([bookClubThread, bobThread], threads)
XCTAssertEqual(1, threads.count)
XCTAssertEqual([bookClubThread], threads)
}
func testSearchMessageByBodyContent() {

@ -35,10 +35,9 @@
- (NSURL *)toSystemDialerURL;
- (NSString *)toE164;
- (NSString *)localizedDescriptionForUser;
- (NSNumber *)getCountryCode;
@property (nonatomic, readonly, nullable) NSString *nationalNumber;
- (BOOL)isValid;
- (BOOL)resolvesInternationallyTo:(PhoneNumber *)otherPhoneNumber;
- (NSComparisonResult)compare:(PhoneNumber *)other;

@ -372,25 +372,23 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN
return self.phoneNumber.countryCode;
}
- (BOOL)isValid {
return [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil isValidNumber:self.phoneNumber];
}
- (NSString *)localizedDescriptionForUser {
NBPhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil;
NSError *formatError = nil;
NSString *pretty =
[phoneUtil format:self.phoneNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:&formatError];
if (formatError != nil) {
return self.e164;
- (nullable NSString *)nationalNumber
{
NSError *error;
NSString *nationalNumber = [[PhoneNumberUtil sharedThreadLocal] format:self.phoneNumber
numberFormat:NBEPhoneNumberFormatNATIONAL
error:&error];
if (error) {
DDLogVerbose(@"%@ error parsing number into national format: %@", self.logTag, error);
return nil;
}
return pretty;
return nationalNumber;
}
- (BOOL)resolvesInternationallyTo:(PhoneNumber *)otherPhoneNumber {
return [self.toE164 isEqualToString:otherPhoneNumber.toE164];
- (BOOL)isValid
{
return [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil isValidNumber:self.phoneNumber];
}
- (NSString *)description {

@ -14,24 +14,39 @@ public class SearchIndexer<T> {
}
public func index(_ item: T) -> String {
return indexBlock(item)
return normalize(indexingText: indexBlock(item))
}
private func normalize(indexingText: String) -> String {
var normalized: String = indexingText.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove any punctuation from the search index
let nonformattingScalars = normalized.unicodeScalars.lazy.filter {
!CharacterSet.punctuationCharacters.contains($0)
}
normalized = String(String.UnicodeScalarView(nonformattingScalars))
return normalized
}
}
@objc
public class FullTextSearchFinder: NSObject {
// Mark: Querying
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
}
let normalized = FullTextSearchFinder.normalize(text: searchText)
let normalized = normalize(queryText: searchText)
// We want a forgiving query for phone numbers
// TODO a stricter "whole word" query for body text?
let prefixQuery = "*\(normalized)*"
// We want to match by prefix for "search as you type" functionality.
// SQLite does not support suffix or contains matches.
let prefixQuery = "\(normalized)*"
let maxSearchResults = 500
var searchResultCount = 0
@ -47,27 +62,31 @@ public class FullTextSearchFinder: NSObject {
}
}
private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? {
return transaction.ext(FullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction
}
// Mark: Index Building
private func normalize(queryText: String) -> String {
var normalized: String = queryText.trimmingCharacters(in: .whitespacesAndNewlines)
private class var contactsManager: ContactsManagerProtocol {
return TextSecureKitEnv.shared().contactsManager
}
private class func normalize(text: String) -> String {
var normalized: String = text.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove any phone number formatting from the search terms
// Remove any punctuation from the search terms
let nonformattingScalars = normalized.unicodeScalars.lazy.filter {
!CharacterSet.punctuationCharacters.contains($0)
}
let normalizedChars = String(String.UnicodeScalarView(nonformattingScalars))
normalized = String(String.UnicodeScalarView(nonformattingScalars))
let digitsOnlyScalars = normalized.unicodeScalars.lazy.filter {
CharacterSet.decimalDigits.contains($0)
}
let normalizedDigits = String(String.UnicodeScalarView(digitsOnlyScalars))
return normalized
if normalizedDigits.count > 0 {
return "\(normalizedChars) OR \(normalizedDigits)"
} else {
return "\(normalizedChars)"
}
}
// Mark: Index Building
private class var contactsManager: ContactsManagerProtocol {
return TextSecureKitEnv.shared().contactsManager
}
private static let groupThreadIndexer: SearchIndexer<TSGroupThread> = SearchIndexer { (groupThread: TSGroupThread) in
@ -77,29 +96,37 @@ public class FullTextSearchFinder: NSObject {
recipientIndexer.index(recipientId)
}.joined(separator: " ")
let searchableContent = "\(groupName) \(memberStrings)"
return normalize(text: searchableContent)
return "\(groupName) \(memberStrings)"
}
private static let contactThreadIndexer: SearchIndexer<TSContactThread> = SearchIndexer { (contactThread: TSContactThread) in
let recipientId = contactThread.contactIdentifier()
let searchableContent = recipientIndexer.index(recipientId)
return normalize(text: searchableContent)
return recipientIndexer.index(recipientId)
}
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String) in
let displayName = contactsManager.displayName(forPhoneIdentifier: recipientId)
let searchableContent = "\(recipientId) \(displayName)"
return normalize(text: searchableContent)
let nationalNumber: String = { (recipientId: String) -> String in
guard let phoneNumber = PhoneNumber(fromE164: recipientId) else {
assertionFailure("unexpected unparseable recipientId: \(recipientId)")
return ""
}
guard let digitScalars = phoneNumber.nationalNumber?.unicodeScalars.filter({ CharacterSet.decimalDigits.contains($0) }) else {
assertionFailure("unexpected unparseable recipientId: \(recipientId)")
return ""
}
return String(String.UnicodeScalarView(digitScalars))
}(recipientId)
return "\(recipientId) \(nationalNumber) \(displayName)"
}
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage) in
let searchableContent = message.body ?? ""
return normalize(text: searchableContent)
return message.body ?? ""
}
private class func indexContent(object: Any) -> String? {
@ -124,8 +151,11 @@ public class FullTextSearchFinder: NSObject {
// MARK: - Extension Registration
// MJK - FIXME - while developing it's helpful to rebuild the index every launch. But we need to remove this before releasing.
private static let dbExtensionName: String = "FullTextSearchFinderExtension\(Date())"
private static let dbExtensionName: String = "FullTextSearchFinderExtension)"
private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? {
return transaction.ext(FullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction
}
@objc
public class func asyncRegisterDatabaseExtension(storage: OWSStorage) {
@ -138,9 +168,6 @@ public class FullTextSearchFinder: NSObject {
}
private class var dbExtensionConfig: YapDatabaseFullTextSearch {
// TODO is it worth doing faceted search, i.e. Author / Name / Content?
// seems unlikely that mobile users would use the "author: Alice" search syntax.
// so for now, everything searchable is jammed into a single column
let contentColumnName = "content"
let handler = YapDatabaseFullTextSearchHandler.withObjectBlock { (dict: NSMutableDictionary, _: String, _: String, object: Any) in

Loading…
Cancel
Save