diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 69bb72832..ef904fa3e 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -230,8 +230,6 @@ 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4574A5D51DD6704700C6B692 /* CallService.swift */; }; 4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */; }; 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45794E851E00620000066731 /* CallUIAdapter.swift */; }; - 45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; }; - 45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; }; 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45847E861E4283C30080EAB3 /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 45855F371D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; }; 45855F381D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; }; @@ -815,8 +813,6 @@ 4579431C1E7C8CE9008ED0C0 /* Pastelog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Pastelog.h; sourceTree = ""; }; 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Pastelog.m; sourceTree = ""; }; 45794E851E00620000066731 /* CallUIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallUIAdapter.swift; path = UserInterface/CallUIAdapter.swift; sourceTree = ""; }; - 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = ""; }; - 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcher.m; sourceTree = ""; }; 45847E861E4283C30080EAB3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; 45855F351D9498A40084F340 /* OWSContactAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactAvatarBuilder.h; sourceTree = ""; }; 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactAvatarBuilder.m; sourceTree = ""; }; @@ -1642,8 +1638,6 @@ children = ( 76EB040818170B33006006FC /* OWSContactsManager.h */, 76EB040918170B33006006FC /* OWSContactsManager.m */, - 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */, - 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */, 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */, ); path = contact; @@ -2770,7 +2764,6 @@ D221A09A169C9E5E00537ABF /* main.m in Sources */, 345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */, 4585C4601ED4FD0400896AEA /* OWS104CreateRecipientIdentities.m in Sources */, - 45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */, B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */, 34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */, 34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */, @@ -2928,7 +2921,6 @@ 456F6E231E24133500FD2210 /* Platform.swift in Sources */, 4539B5871F79348F007141FF /* PushRegistrationManager.swift in Sources */, 4504493A1F45EE7D002D1ADA /* NSString+OWS.m in Sources */, - 45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */, 45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */, 45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */, B660F7561C29988E00687D6E /* PushManager.m in Sources */, diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m index f1bfdb0f4..50dafced6 100644 --- a/Signal/src/ViewControllers/ContactsViewHelper.m +++ b/Signal/src/ViewControllers/ContactsViewHelper.m @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts; @property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce; @property (nonatomic) OWSProfileManager *profileManager; -@property (nonatomic, readonly) AnySearcher *signalAccountSearcher; +@property (nonatomic, readonly) ConversationSearcher *conversationSearcher; @end @@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN _blockingManager = [OWSBlockingManager sharedManager]; _blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers]; + _conversationSearcher = ConversationSearcher.shared; _contactsManager = [Environment getCurrent].contactsManager; _profileManager = [OWSProfileManager sharedManager]; @@ -60,8 +61,6 @@ NS_ASSUME_NONNULL_BEGIN [self updateContacts]; self.shouldNotifyDelegateOfUpdatedContacts = NO; - _signalAccountSearcher = [self buildSignalAccountSearcher]; - [self observeNotifications]; return self; @@ -102,24 +101,6 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Contacts -- (AnySearcher *)buildSignalAccountSearcher -{ - return [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) { - if (![obj isKindOfClass:[SignalAccount class]]) { - OWSFail(@"unexpected item in searcher"); - return @""; - } - - SignalAccount *signalAccount = (SignalAccount *)obj; - - NSString *recipientId = signalAccount.recipientId; - NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:recipientId]; - NSString *profileName = [self.contactsManager profileNameForRecipientId:recipientId]; - - return [NSString stringWithFormat:@"%@ %@ %@", recipientId, contactName, profileName]; - }]; -} - - (nullable SignalAccount *)signalAccountForRecipientId:(NSString *)recipientId { OWSAssert([NSThread isMainThread]); @@ -206,17 +187,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)signalAccountsMatchingSearchString:(NSString *)searchText { - NSArray *searchTerms = [self searchTermsForSearchString:searchText]; - - if (searchTerms.count < 1) { - return self.signalAccounts; - } - - return [self.signalAccounts - filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(SignalAccount *signalAccount, - NSDictionary *_Nullable bindings) { - return [self.signalAccountSearcher item:signalAccount doesMatchQuery:searchText]; - }]]; + return [self.conversationSearcher filterSignalAccounts:self.signalAccounts withSearchText:searchText]; } - (BOOL)doesContact:(Contact *)contact matchSearchTerm:(NSString *)searchTerm diff --git a/Signal/src/ViewControllers/NewContactThreadViewController.m b/Signal/src/ViewControllers/NewContactThreadViewController.m index 9b65ec0a6..b02aa5977 100644 --- a/Signal/src/ViewControllers/NewContactThreadViewController.m +++ b/Signal/src/ViewControllers/NewContactThreadViewController.m @@ -8,7 +8,6 @@ #import "Environment.h" #import "NewGroupViewController.h" #import "NewNonContactConversationViewController.h" -#import "OWSContactsSearcher.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "UIColor+OWS.h" @@ -45,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN MFMessageComposeViewControllerDelegate> @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; +@property (nonatomic, readonly) ConversationSearcher *conversationSearcher; @property (nonatomic, readonly) UIView *noSignalContactsView; @@ -77,6 +77,7 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = UIColor.whiteColor; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; + _conversationSearcher = [ConversationSearcher shared]; _nonContactAccountSet = [NSMutableSet set]; _collation = [UILocalizedIndexedCollation currentCollation]; @@ -630,35 +631,17 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)filteredGroupThreads { - AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString * _Nonnull(id _Nonnull obj) { - if (![obj isKindOfClass:[TSGroupThread class]]) { - OWSFail(@"unexpected item in searcher"); - return @""; - } - TSGroupThread *groupThread = (TSGroupThread *)obj; - NSString *groupName = groupThread.groupModel.groupName; - NSMutableString *groupMemberNames = [NSMutableString new]; - for (NSString *recipientId in groupThread.groupModel.groupMemberIds) { - NSString *contactName = [self.contactsViewHelper.contactsManager displayNameForPhoneIdentifier:recipientId]; - [groupMemberNames appendFormat:@" %@", contactName]; - } - - return [NSString stringWithFormat:@"%@ %@", groupName, groupMemberNames]; - }]; - - NSMutableArray *matchingThreads = [NSMutableArray new]; + NSMutableArray *groupThreads = [NSMutableArray new]; [TSGroupThread enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) { if (![obj isKindOfClass:[TSGroupThread class]]) { // group and contact threads are in the same collection. return; } TSGroupThread *groupThread = (TSGroupThread *)obj; - if ([searcher item:groupThread doesMatchQuery:self.searchBar.text]) { - [matchingThreads addObject:groupThread]; - } + [groupThreads addObject:groupThread]; }]; - return [matchingThreads copy]; + return [self.conversationSearcher filterGroupThreads:groupThreads withSearchText:self.searchBar.text]; } #pragma mark - No Contacts Mode diff --git a/Signal/src/ViewControllers/SelectThreadViewController.m b/Signal/src/ViewControllers/SelectThreadViewController.m index c39e374f9..dfbc5a158 100644 --- a/Signal/src/ViewControllers/SelectThreadViewController.m +++ b/Signal/src/ViewControllers/SelectThreadViewController.m @@ -9,8 +9,8 @@ #import "Environment.h" #import "NSString+OWS.h" #import "OWSContactsManager.h" -#import "OWSContactsSearcher.h" #import "OWSTableViewController.h" +#import "Signal-Swift.h" #import "ThreadViewHelper.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN UISearchBarDelegate> @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; - +@property (nonatomic, readonly) ConversationSearcher *conversationSearcher; @property (nonatomic, readonly) ThreadViewHelper *threadViewHelper; @property (nonatomic, readonly) OWSTableViewController *tableViewController; @@ -54,6 +54,7 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = [UIColor whiteColor]; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; + _conversationSearcher = ConversationSearcher.shared; _threadViewHelper = [ThreadViewHelper new]; _threadViewHelper.delegate = self; @@ -132,7 +133,7 @@ NS_ASSUME_NONNULL_BEGIN ContactsViewHelper *helper = self.contactsViewHelper; OWSTableContents *contents = [OWSTableContents new]; - // Threads are listed, most recent first. + // Existing threads are listed first, ordered by most recently active OWSTableSection *recentChatsSection = [OWSTableSection new]; recentChatsSection.headerTitle = NSLocalizedString( @"SELECT_THREAD_TABLE_RECENT_CHATS_TITLE", @"Table section header for recently active conversations"); @@ -237,7 +238,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)filteredThreadsWithSearchText { NSString *searchTerm = [[self.searchBar text] ows_stripped]; - return [self.threadViewHelper threadsMatchingSearchString:searchTerm]; + + return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm]; } - (NSArray *)filteredSignalAccountsWithSearchText diff --git a/Signal/src/ViewControllers/ThreadViewHelper.h b/Signal/src/ViewControllers/ThreadViewHelper.h index 917ccf3de..6b73af8c4 100644 --- a/Signal/src/ViewControllers/ThreadViewHelper.h +++ b/Signal/src/ViewControllers/ThreadViewHelper.h @@ -4,8 +4,6 @@ NS_ASSUME_NONNULL_BEGIN -@class AnySearcher; - @protocol ThreadViewHelperDelegate - (void)threadListDidChange; @@ -25,10 +23,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id delegate; @property (nonatomic, readonly) NSMutableArray *threads; -@property (nonatomic, readonly) AnySearcher *groupThreadSearcher; -@property (nonatomic, readonly) AnySearcher *contactThreadSearcher; - -- (NSArray *)threadsMatchingSearchString:(NSString *)searchString; @end diff --git a/Signal/src/ViewControllers/ThreadViewHelper.m b/Signal/src/ViewControllers/ThreadViewHelper.m index 9bae7c91f..3df98fdfe 100644 --- a/Signal/src/ViewControllers/ThreadViewHelper.m +++ b/Signal/src/ViewControllers/ThreadViewHelper.m @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) YapDatabaseConnection *uiDatabaseConnection; @property (nonatomic) YapDatabaseViewMappings *threadMappings; +@property (nonatomic) ConversationSearcher *conversationSearcher; @end @@ -30,8 +31,7 @@ NS_ASSUME_NONNULL_BEGIN } [self initializeMapping]; - _groupThreadSearcher = [self buildGroupThreadSearcher]; - _contactThreadSearcher = [self buildContactThreadSearcher]; + _conversationSearcher = ConversationSearcher.shared; return self; } @@ -126,76 +126,6 @@ NS_ASSUME_NONNULL_BEGIN _threads = [threads copy]; } -#pragma mark - Searching - -- (OWSContactsManager *)contactsManager -{ - return [Environment getCurrent].contactsManager; -} - -- (NSString *)searchIndexStringForRecipientId:(NSString *)recipientId -{ - NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:recipientId]; - NSString *profileName = [self.contactsManager profileNameForRecipientId:recipientId]; - - return [NSString stringWithFormat:@"%@ %@ %@", recipientId, contactName, profileName]; -} - -- (AnySearcher *)buildContactThreadSearcher -{ - AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) { - if (![obj isKindOfClass:[TSContactThread class]]) { - OWSFail(@"unexpected item in searcher"); - return @""; - } - TSContactThread *contactThread = (TSContactThread *)obj; - - NSString *recipientId = contactThread.contactIdentifier; - return [self searchIndexStringForRecipientId:recipientId]; - }]; - - return searcher; -} - -- (AnySearcher *)buildGroupThreadSearcher -{ - AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) { - if (![obj isKindOfClass:[TSGroupThread class]]) { - OWSFail(@"unexpected item in searcher"); - return @""; - } - TSGroupThread *groupThread = (TSGroupThread *)obj; - NSString *groupName = groupThread.groupModel.groupName; - NSMutableString *groupMemberStrings = [NSMutableString new]; - for (NSString *recipientId in groupThread.groupModel.groupMemberIds) { - NSString *recipientString = [self searchIndexStringForRecipientId:recipientId]; - [groupMemberStrings appendFormat:@" %@", recipientString]; - } - - return [NSString stringWithFormat:@"%@ %@", groupName, groupMemberStrings]; - }]; - - return searcher; -} - -- (NSArray *)threadsMatchingSearchString:(NSString *)searchString -{ - if (searchString.length == 0) { - return self.threads; - } - - NSMutableArray *result = [NSMutableArray new]; - for (TSThread *thread in self.threads) { - AnySearcher *searcher = - [thread isKindOfClass:[TSContactThread class]] ? self.contactThreadSearcher : self.groupThreadSearcher; - if ([searcher item:thread doesMatchQuery:searchString]) { - [result addObject:thread]; - } - } - return result; -} - - @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/contact/OWSContactsSearcher.h b/Signal/src/contact/OWSContactsSearcher.h deleted file mode 100644 index 09f96e0d2..000000000 --- a/Signal/src/contact/OWSContactsSearcher.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// OWSContactsSearcher.h -// Signal -// -// Created by Michael Kirk on 6/27/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. -// - -#import "Contact.h" - -@interface OWSContactsSearcher : NSObject - -- (instancetype)initWithContacts:(NSArray *)contacts; -- (NSArray *)filterWithString:(NSString *)string; - -@end diff --git a/Signal/src/contact/OWSContactsSearcher.m b/Signal/src/contact/OWSContactsSearcher.m deleted file mode 100644 index 774f1038c..000000000 --- a/Signal/src/contact/OWSContactsSearcher.m +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "OWSContactsSearcher.h" -#import "NSString+OWS.h" -#import - -@interface OWSContactsSearcher () - -@property (copy) NSArray *contacts; - -@end - -@implementation OWSContactsSearcher - -- (instancetype)initWithContacts:(NSArray *)contacts { - self = [super init]; - if (!self) return self; - - _contacts = contacts; - return self; -} - -- (NSArray *)filterWithString:(NSString *)string { - NSString *searchTerm = [string ows_stripped]; - - if ([searchTerm isEqualToString:@""]) { - return self.contacts; - } - - NSString *formattedNumber = [PhoneNumber removeFormattingCharacters:searchTerm]; - - // TODO: This assumes there's a single search term. - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(fullName contains[c] %@) OR (ANY parsedPhoneNumbers.toE164 contains[c] %@)", searchTerm, formattedNumber]; - - return [self.contacts filteredArrayUsingPredicate:predicate]; -} - -@end diff --git a/Signal/src/util/Searcher.swift b/Signal/src/util/Searcher.swift index aab5fa7fb..7adf75fc7 100644 --- a/Signal/src/util/Searcher.swift +++ b/Signal/src/util/Searcher.swift @@ -3,6 +3,91 @@ // import Foundation +import SignalServiceKit + +@objc +class ConversationSearcher: NSObject { + + @objc + public static let shared: ConversationSearcher = ConversationSearcher() + override private init() { + super.init() + } + + @objc(filterThreads:withSearchText:) + public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] { + guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else { + return threads + } + + return threads.filter { thread in + switch thread { + case let groupThread as TSGroupThread: + return self.groupThreadSearcher.matches(item: groupThread, query: searchText) + case let contactThread as TSContactThread: + return self.contactThreadSearcher.matches(item: contactThread, query: searchText) + default: + owsFail("Unexpected thread type: \(thread)") + return false + } + } + } + + @objc(filterGroupThreads:withSearchText:) + public func filterGroupThreads(_ groupThreads: [TSGroupThread], searchText: String) -> [TSGroupThread] { + guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else { + return groupThreads + } + + return groupThreads.filter { groupThread in + return self.groupThreadSearcher.matches(item: groupThread, query: searchText) + } + } + + @objc(filterSignalAccounts:withSearchText:) + public func filterSignalAccounts(_ signalAccounts: [SignalAccount], searchText: String) -> [SignalAccount] { + guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else { + return signalAccounts + } + + return signalAccounts.filter { signalAccount in + self.signalAccountSearcher.matches(item: signalAccount, query: searchText) + } + } + + // MARK: - Helpers + + // MARK: Searchers + private lazy var groupThreadSearcher: Searcher = Searcher { (groupThread: TSGroupThread) in + let groupName = groupThread.groupModel.groupName + let memberStrings = groupThread.groupModel.groupMemberIds.map { recipientId in + self.indexingString(recipientId: recipientId) + }.joined(separator: " ") + + return "\(memberStrings) \(groupName ?? "")" + } + + private lazy var contactThreadSearcher: Searcher = Searcher { (contactThread: TSContactThread) in + let recipientId = contactThread.contactIdentifier() + return self.indexingString(recipientId: recipientId) + } + + private lazy var signalAccountSearcher: Searcher = Searcher { (signalAccount: SignalAccount) in + let recipientId = signalAccount.recipientId + return self.indexingString(recipientId: recipientId) + } + + private var contactsManager: OWSContactsManager { + return Environment.getCurrent().contactsManager + } + + private func indexingString(recipientId: String) -> String { + let contactName = contactsManager.displayName(forPhoneIdentifier: recipientId) + let profileName = contactsManager.profileName(forRecipientId: recipientId) + + return "\(recipientId) \(contactName) \(profileName ?? "")" + } +} // ObjC compatible searcher @objc class AnySearcher: NSObject { @@ -19,6 +104,7 @@ import Foundation } } +// A generic searching class, configurable with an indexing block class Searcher { private let indexer: (T) -> String