diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index d79e7e515..e140ffea0 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 */; }; @@ -257,6 +255,8 @@ 45A6DAD71EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; }; 45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; }; 45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; }; + 45B72DDA1FD5E70600151AF6 /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B72DD91FD5E70600151AF6 /* ConversationSearcher.swift */; }; + 45B72DDB1FD5E70600151AF6 /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B72DD91FD5E70600151AF6 /* ConversationSearcher.swift */; }; 45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; }; 45BB93391E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; }; 45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -279,7 +279,6 @@ 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */; }; 45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; }; 45F170AD1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; }; - 45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */; }; 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F170CC1E310E22003FC1F2 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */; }; @@ -816,8 +815,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 = ""; }; @@ -849,6 +846,7 @@ 45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = ""; }; 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = ""; }; + 45B72DD91FD5E70600151AF6 /* ConversationSearcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = ""; }; 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+featureSupport.swift"; sourceTree = ""; }; 45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; 45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+OWS.swift"; sourceTree = ""; }; @@ -866,7 +864,6 @@ 45E615151E8C590B0018AD52 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableText.swift; sourceTree = ""; }; 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableTextFilterTest.swift; sourceTree = ""; }; 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; - 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSessionTest.swift; sourceTree = ""; }; 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = ""; }; 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; @@ -1644,8 +1641,6 @@ children = ( 76EB040818170B33006006FC /* OWSContactsManager.h */, 76EB040918170B33006006FC /* OWSContactsManager.m */, - 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */, - 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */, 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */, ); path = contact; @@ -1725,6 +1720,7 @@ 76EB04FB18170B33006006FC /* Util.h */, 45F170D51E315310003FC1F2 /* Weak.swift */, 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, + 45B72DD91FD5E70600151AF6 /* ConversationSearcher.swift */, ); path = util; sourceTree = ""; @@ -1837,7 +1833,6 @@ B660F6731C29867F00687D6E /* call */ = { isa = PBXGroup; children = ( - 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */, 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */, ); path = call; @@ -2719,6 +2714,7 @@ 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, 34D1F0821F8678AA0066283D /* ConversationHeaderView.m in Sources */, 340CB2241EAC155C0001CAA1 /* ContactsViewHelper.m in Sources */, + 45B72DDA1FD5E70600151AF6 /* ConversationSearcher.swift in Sources */, 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */, 34CE88EC1F3237260098030F /* OWSProfileManager.m in Sources */, 4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */, @@ -2773,7 +2769,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,11 +2923,9 @@ 458E383A1D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m in Sources */, 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */, 451DE9FE1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */, - 45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */, 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 */, @@ -2978,6 +2971,7 @@ 45A6DAD71EBBF85500893231 /* ReminderView.swift in Sources */, B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, 45C0DC1F1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, + 45B72DDB1FD5E70600151AF6 /* ConversationSearcher.swift in Sources */, 4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */, ); diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m index 1a3f1b3fd..7fc14b62a 100644 --- a/Signal/src/ViewControllers/ContactsViewHelper.m +++ b/Signal/src/ViewControllers/ContactsViewHelper.m @@ -3,7 +3,6 @@ // #import "ContactsViewHelper.h" -#import "ContactTableViewCell.h" #import "Environment.h" #import "NSString+OWS.h" #import "OWSProfileManager.h" @@ -31,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts; @property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce; @property (nonatomic) OWSProfileManager *profileManager; +@property (nonatomic, readonly) ConversationSearcher *conversationSearcher; @end @@ -50,6 +50,7 @@ NS_ASSUME_NONNULL_BEGIN _blockingManager = [OWSBlockingManager sharedManager]; _blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers]; + _conversationSearcher = ConversationSearcher.shared; _contactsManager = [Environment getCurrent].contactsManager; _profileManager = [OWSProfileManager sharedManager]; @@ -173,37 +174,6 @@ NS_ASSUME_NONNULL_BEGIN } } -- (BOOL)doesSignalAccount:(SignalAccount *)signalAccount matchSearchTerm:(NSString *)searchTerm -{ - OWSAssert(signalAccount); - OWSAssert(searchTerm.length > 0); - - if ([signalAccount.contact.fullName.lowercaseString containsString:searchTerm.lowercaseString]) { - return YES; - } - - NSString *asPhoneNumber = [PhoneNumber removeFormattingCharacters:searchTerm]; - if (asPhoneNumber.length > 0 && [signalAccount.recipientId containsString:asPhoneNumber]) { - return YES; - } - - return NO; -} - -- (BOOL)doesSignalAccount:(SignalAccount *)signalAccount matchSearchTerms:(NSArray *)searchTerms -{ - OWSAssert(signalAccount); - OWSAssert(searchTerms.count > 0); - - for (NSString *searchTerm in searchTerms) { - if (![self doesSignalAccount:signalAccount matchSearchTerm:searchTerm]) { - return NO; - } - } - - return YES; -} - - (NSArray *)searchTermsForSearchString:(NSString *)searchText { return [[[searchText ows_stripped] @@ -216,17 +186,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 doesSignalAccount:signalAccount matchSearchTerms:searchTerms]; - }]]; + 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 a68ce4151..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; @@ -131,11 +132,13 @@ NS_ASSUME_NONNULL_BEGIN __weak SelectThreadViewController *weakSelf = self; ContactsViewHelper *helper = self.contactsViewHelper; OWSTableContents *contents = [OWSTableContents new]; - OWSTableSection *section = [OWSTableSection new]; - // Threads + // 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"); for (TSThread *thread in [self filteredThreadsWithSearchText]) { - [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ + [recentChatsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ SelectThreadViewController *strongSelf = weakSelf; OWSCAssert(strongSelf); @@ -145,16 +148,23 @@ NS_ASSUME_NONNULL_BEGIN [cell configureWithThread:thread contactsManager:helper.contactsManager]; return cell; } - customRowHeight:[ContactTableViewCell rowHeight] - actionBlock:^{ - [weakSelf.delegate threadWasSelected:thread]; - }]]; + customRowHeight:[ContactTableViewCell rowHeight] + actionBlock:^{ + [weakSelf.delegate threadWasSelected:thread]; + }]]; + } + + if (recentChatsSection.itemCount > 0) { + [contents addSection:recentChatsSection]; } - // Contacts + // Contacts who don't yet have a thread are listed last + OWSTableSection *otherContactsSection = [OWSTableSection new]; + otherContactsSection.headerTitle = NSLocalizedString( + @"SELECT_THREAD_TABLE_OTHER_CHATS_TITLE", @"Table section header for conversations you haven't recently used."); NSArray *filteredSignalAccounts = [self filteredSignalAccountsWithSearchText]; for (SignalAccount *signalAccount in filteredSignalAccounts) { - [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ + [otherContactsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ SelectThreadViewController *strongSelf = weakSelf; OWSCAssert(strongSelf); @@ -169,19 +179,24 @@ NS_ASSUME_NONNULL_BEGIN [cell configureWithSignalAccount:signalAccount contactsManager:helper.contactsManager]; return cell; } - customRowHeight:[ContactTableViewCell rowHeight] - actionBlock:^{ - [weakSelf signalAccountWasSelected:signalAccount]; - }]]; + customRowHeight:[ContactTableViewCell rowHeight] + actionBlock:^{ + [weakSelf signalAccountWasSelected:signalAccount]; + }]]; + } + + if (otherContactsSection.itemCount > 0) { + [contents addSection:otherContactsSection]; } - if (section.itemCount < 1) { - [section + if (recentChatsSection.itemCount + otherContactsSection.itemCount < 1) { + OWSTableSection *emptySection = [OWSTableSection new]; + [emptySection addItem:[OWSTableItem softCenterLabelItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_CONTACTS", @"A label that indicates the user has no Signal contacts.")]]; + [contents addSection:emptySection]; } - [contents addSection:section]; self.tableViewController.contents = contents; } @@ -222,28 +237,9 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)filteredThreadsWithSearchText { - NSArray *threads = self.threadViewHelper.threads; - NSString *searchTerm = [[self.searchBar text] ows_stripped]; - if ([searchTerm isEqualToString:@""]) { - return threads; - } - - NSString *formattedNumber = [PhoneNumber removeFormattingCharacters:searchTerm]; - - NSMutableArray *result = [NSMutableArray new]; - for (TSThread *thread in threads) { - if ([thread.name containsString:searchTerm]) { - [result addObject:thread]; - } else if ([thread isKindOfClass:[TSContactThread class]]) { - TSContactThread *contactThread = (TSContactThread *)thread; - if (formattedNumber.length > 0 && [contactThread.contactIdentifier containsString:formattedNumber]) { - [result addObject:thread]; - } - } - } - return result; + return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm]; } - (NSArray *)filteredSignalAccountsWithSearchText @@ -259,10 +255,11 @@ NS_ASSUME_NONNULL_BEGIN } } - NSString *searchString = [self.searchBar text]; + NSString *searchString = self.searchBar.text; + NSArray *matchingAccounts = + [self.contactsViewHelper signalAccountsMatchingSearchString:searchString]; - ContactsViewHelper *helper = self.contactsViewHelper; - return [[helper signalAccountsMatchingSearchString:searchString] + return [matchingAccounts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(SignalAccount *signalAccount, NSDictionary *_Nullable bindings) { return ![contactIdsToIgnore containsObject:signalAccount.recipientId]; 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/ConversationSearcher.swift b/Signal/src/util/ConversationSearcher.swift new file mode 100644 index 000000000..7464850de --- /dev/null +++ b/Signal/src/util/ConversationSearcher.swift @@ -0,0 +1,90 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +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 ?? "")" + } +} diff --git a/Signal/src/util/Searcher.swift b/Signal/src/util/Searcher.swift index 0cd260025..f38956c50 100644 --- a/Signal/src/util/Searcher.swift +++ b/Signal/src/util/Searcher.swift @@ -19,6 +19,7 @@ import Foundation } } +// A generic searching class, configurable with an indexing block class Searcher { private let indexer: (T) -> String @@ -36,7 +37,16 @@ class Searcher { } private func stem(string: String) -> [String] { - return normalize(string: string).components(separatedBy: .whitespaces) + var normalized = normalize(string: string) + + // Remove any phone number formatting from the search terms + let nonformattingScalars = normalized.unicodeScalars.lazy.filter { + !CharacterSet.punctuationCharacters.contains($0) + } + + normalized = String(String.UnicodeScalarView(nonformattingScalars)) + + return normalized.components(separatedBy: .whitespacesAndNewlines) } private func normalize(string: String) -> String { diff --git a/Signal/src/views/ContactTableViewCell.m b/Signal/src/views/ContactTableViewCell.m index 0f81b7774..f32996405 100644 --- a/Signal/src/views/ContactTableViewCell.m +++ b/Signal/src/views/ContactTableViewCell.m @@ -170,6 +170,8 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; self.nameLabel.attributedText = attributedText; if ([thread isKindOfClass:[TSContactThread class]]) { + self.recipientId = thread.contactIdentifier; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherUsersProfileDidChange:) name:kNSNotificationName_OtherUsersProfileDidChange @@ -220,6 +222,7 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; diameter:kContactTableViewCellAvatarSize contactsManager:contactsManager] build]; } + - (void)updateProfileName { OWSContactsManager *contactsManager = self.contactsManager; diff --git a/Signal/test/Models/AccountManagerTest.swift b/Signal/test/Models/AccountManagerTest.swift index 3ac8a62d0..7a9589ffb 100644 --- a/Signal/test/Models/AccountManagerTest.swift +++ b/Signal/test/Models/AccountManagerTest.swift @@ -4,6 +4,7 @@ import XCTest import PromiseKit +import SignalServiceKit struct VerificationFailedError: Error { } struct FailedToGetRPRegistrationTokenError: Error { } diff --git a/Signal/test/Models/MesssagesBubblesSizeCalculatorTest.swift b/Signal/test/Models/MesssagesBubblesSizeCalculatorTest.swift index f4e96934b..9ee7327ec 100644 --- a/Signal/test/Models/MesssagesBubblesSizeCalculatorTest.swift +++ b/Signal/test/Models/MesssagesBubblesSizeCalculatorTest.swift @@ -3,6 +3,7 @@ // import XCTest +import SignalServiceKit /** * This is a brittle test, which will break if our layout changes. @@ -23,7 +24,12 @@ class MesssagesBubblesSizeCalculatorTest: XCTestCase { func viewItemForText(_ text: String?) -> ConversationViewItem { let interaction = TSOutgoingMessage(timestamp: 0, in: thread, messageBody: text) interaction.save() - let viewItem = ConversationViewItem(tsInteraction:interaction, isGroupThread:false) + + var viewItem: ConversationViewItem! + interaction.dbReadWriteConnection().readWrite { transaction in + viewItem = ConversationViewItem(interaction: interaction, isGroupThread: false, transaction: transaction) + } + viewItem.shouldShowDate = false viewItem.shouldHideRecipientStatus = true return viewItem diff --git a/Signal/test/ViewControllers/ConversationViewItemTest.m b/Signal/test/ViewControllers/ConversationViewItemTest.m index 3914015e8..df293aaec 100644 --- a/Signal/test/ViewControllers/ConversationViewItemTest.m +++ b/Signal/test/ViewControllers/ConversationViewItemTest.m @@ -8,6 +8,7 @@ #import #import #import +#import @interface ConversationViewItem (Testing) @@ -64,8 +65,10 @@ OWSAssert([[NSFileManager defaultManager] fileExistsAtPath:filePath]); - TSAttachmentStream *attachment = [[TSAttachmentStream alloc] initWithContentType:mimeType sourceFilename:nil]; DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:filePath]; + TSAttachmentStream *attachment = [[TSAttachmentStream alloc] initWithContentType:mimeType + byteCount:(UInt32)dataSource.dataLength + sourceFilename:nil]; BOOL success = [attachment writeDataSource:dataSource]; OWSAssert(success); [attachment save]; @@ -75,7 +78,12 @@ TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1 inThread:nil messageBody:nil attachmentIds:attachmentIds]; [message save]; - ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:message isGroupThread:NO]; + + __block ConversationViewItem *viewItem = nil; + [TSYapDatabaseObject.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + viewItem = [[ConversationViewItem alloc] initWithInteraction:message isGroupThread:NO transaction:transaction]; + }]; + return viewItem; } diff --git a/Signal/test/call/CallAudioSessionTest.swift b/Signal/test/call/CallAudioSessionTest.swift deleted file mode 100644 index 7e3e6d042..000000000 --- a/Signal/test/call/CallAudioSessionTest.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2017 Open Whisper Systems. All rights reserved. -// - -import XCTest -import AVKit -import WebRTC - -/** - * These tests are obtuse - they just assert the exact implementation of the methods. Normally I wouldn't include them, - * but these methods make use of a header not included in the standard distribution of the WebRTC.framework. We've - * included the header in our local project, and test the methods here to make sure that they are still available when - * we upgrade the framework. - * - * If they are failing, it's possible the RTCAudioSession header, and our usage of it, need to be updated. - */ -class CallAudioSessionTest: XCTestCase { - func testAudioSession() { - - let rtcAudioSession = RTCAudioSession.sharedInstance() - // Sanity Check - XCTAssertFalse(rtcAudioSession.useManualAudio) - - CallAudioSession().configure() - XCTAssertTrue(rtcAudioSession.useManualAudio) - XCTAssertFalse(rtcAudioSession.isAudioEnabled) - - CallAudioSession().start() - XCTAssertTrue(rtcAudioSession.useManualAudio) - XCTAssertTrue(rtcAudioSession.isAudioEnabled) - - CallAudioSession().stop() - XCTAssertTrue(rtcAudioSession.useManualAudio) - XCTAssertFalse(rtcAudioSession.isAudioEnabled) - } -} diff --git a/Signal/test/util/SearcherTest.swift b/Signal/test/util/SearcherTest.swift index 0be0cdb44..8e12ac952 100644 --- a/Signal/test/util/SearcherTest.swift +++ b/Signal/test/util/SearcherTest.swift @@ -9,14 +9,15 @@ class SearcherTest: XCTestCase { struct TestCharacter { let name: String let description: String + let phoneNumber: String? } - let smerdyakov = TestCharacter(name: "Pavel Fyodorovich Smerdyakov", description: "A rusty hue in the sky") - let stinkingLizaveta = TestCharacter(name: "Stinking Lizaveta", description: "object of pity") - let regularLizaveta = TestCharacter(name: "Lizaveta", description: "") + let smerdyakov = TestCharacter(name: "Pavel Fyodorovich Smerdyakov", description: "A rusty hue in the sky", phoneNumber: nil) + let stinkingLizaveta = TestCharacter(name: "Stinking Lizaveta", description: "object of pity", phoneNumber: "+13235555555") + let regularLizaveta = TestCharacter(name: "Lizaveta", description: "", phoneNumber: "1 (415) 555-5555") let indexer = { (character: TestCharacter) in - return "\(character.name) \(character.description)" + return "\(character.name) \(character.description) \(character.phoneNumber ?? "")" } var searcher: Searcher { @@ -57,4 +58,20 @@ class SearcherTest: XCTestCase { XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta St")) XCTAssert(searcher.matches(item: stinkingLizaveta, query: " Lizaveta St ")) } + + func testFormattingChars() { + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"323")) + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"1-323-555-5555")) + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"13235555555")) + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"+1-323")) + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"Liza +1-323")) + + // Sanity check, match both by names + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"Liza")) + XCTAssert(searcher.matches(item: regularLizaveta, query:"Liza")) + + // Disambiguate the two Liza's by area code + XCTAssert(searcher.matches(item: stinkingLizaveta, query:"Liza 323")) + XCTAssertFalse(searcher.matches(item: regularLizaveta, query:"Liza 323")) + } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index c1dd815d5..5c8334918 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1342,6 +1342,12 @@ /* Label for 'select gif to attach' action sheet button */ "SELECT_GIF_BUTTON" = "GIF"; +/* Table section header for conversations you haven't recently used. */ +"SELECT_THREAD_TABLE_OTHER_CHATS_TITLE" = "Other Contacts"; + +/* Table section header for recently active conversations */ +"SELECT_THREAD_TABLE_RECENT_CHATS_TITLE" = "Recent Chats"; + /* No comment provided by engineer. */ "SEND_AGAIN_BUTTON" = "Send Again"; diff --git a/SignalMessaging/utils/DebugLogger.m b/SignalMessaging/utils/DebugLogger.m index a4be908e9..5e2fb16ad 100644 --- a/SignalMessaging/utils/DebugLogger.m +++ b/SignalMessaging/utils/DebugLogger.m @@ -106,7 +106,7 @@ // This should be redundant with the logic above. [logPathSet addObjectsFromArray:self.fileLogger.logFileManager.unsortedLogFilePaths]; NSArray *logPaths = logPathSet.allObjects; - return [logPaths sortedArrayUsingSelector:@selector(compare:)]; + return [logPaths sortedArrayUsingSelector:@selector((compare:))]; } - (void)wipeLogs diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index ef09171cc..e779d2dbb 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -320,8 +320,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; // Store the file in a subdirectory whose name is the uniqueId of this attachment, // to avoid collisions between multiple attachments with the same name. NSString *attachmentFolderPath = [folder stringByAppendingPathComponent:uniqueId]; - NSError *error = nil; - BOOL attachmentFolderPathExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFolderPath]; if (![OWSFileSystem ensureDirectoryExists:attachmentFolderPath]) { return nil; }