diff --git a/Signal/src/ViewControllers/NewContactThreadViewController.m b/Signal/src/ViewControllers/NewContactThreadViewController.m index 340e59dde..af128a547 100644 --- a/Signal/src/ViewControllers/NewContactThreadViewController.m +++ b/Signal/src/ViewControllers/NewContactThreadViewController.m @@ -54,6 +54,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) UILocalizedIndexedCollation *collation; @property (nonatomic, readonly) UISearchBar *searchBar; +@property (nonatomic) ComposeScreenSearchResultSet *searchResults; // A list of possible phone numbers parsed from the search text as // E164 values. @@ -71,12 +72,32 @@ NS_ASSUME_NONNULL_BEGIN @implementation NewContactThreadViewController + +#pragma mark - Dependencies + +- (ConversationSearcher *)conversationSearcher +{ + return ConversationSearcher.shared; +} + +- (YapDatabaseConnection *)uiDatabaseConnection +{ + return OWSPrimaryStorage.sharedManager.uiDatabaseConnection; +} + +- (OWSContactsManager *)contactsManager +{ + return Environment.shared.contactsManager; +} + +#pragma mark - + - (void)loadView { [super loadView]; + _searchResults = ComposeScreenSearchResultSet.empty; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; - _conversationSearcher = [ConversationSearcher shared]; _nonContactAccountSet = [NSMutableSet set]; _collation = [UILocalizedIndexedCollation currentCollation]; @@ -155,13 +176,12 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertIsOnMainThread(); - [self.contactsViewHelper.contactsManager - userRequestedSystemContactsRefreshWithCompletion:^(NSError *_Nullable error) { - if (error) { - OWSLogError(@"refreshing contacts failed with error: %@", error); - } - [refreshControl endRefreshing]; - }]; + [self.contactsManager userRequestedSystemContactsRefreshWithCompletion:^(NSError *_Nullable error) { + if (error) { + OWSLogError(@"refreshing contacts failed with error: %@", error); + } + [refreshControl endRefreshing]; + }]; } - (void)showSearchBar:(BOOL)isVisible @@ -268,7 +288,7 @@ NS_ASSUME_NONNULL_BEGIN // Make sure we have requested contact access at this point if, e.g. // the user has no messages in their inbox and they choose to compose // a message. - [self.contactsViewHelper.contactsManager requestSystemContactsOnce]; + [self.contactsManager requestSystemContactsOnce]; [self showContactAppropriateViews]; } @@ -295,7 +315,7 @@ NS_ASSUME_NONNULL_BEGIN // App is killed and restarted when the user changes their contact permissions, so need need to "observe" anything // to re-render this. - if (self.contactsViewHelper.contactsManager.isSystemContactsDenied) { + if (self.contactsManager.isSystemContactsDenied) { OWSTableItem *contactReminderItem = [OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *newCell = [OWSTableItem newCell]; @@ -334,7 +354,7 @@ NS_ASSUME_NONNULL_BEGIN animated:YES]; }]]; - if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized) { + if (self.contactsManager.isSystemContactsAuthorized) { // Invite Contacts [staticSection addItem:[OWSTableItem @@ -347,7 +367,7 @@ NS_ASSUME_NONNULL_BEGIN } [contents addSection:staticSection]; - BOOL hasSearchText = [self.searchBar text].length > 0; + BOOL hasSearchText = self.searchText.length > 0; if (hasSearchText) { for (OWSTableSection *section in [self contactsSectionsForSearch]) { @@ -404,7 +424,7 @@ NS_ASSUME_NONNULL_BEGIN // No Contacts OWSTableSection *contactsSection = [OWSTableSection new]; - if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized) { + if (self.contactsManager.isSystemContactsAuthorized) { if (self.contactsViewHelper.hasUpdatedContactsAtLeastOnce) { [contactsSection @@ -433,7 +453,7 @@ NS_ASSUME_NONNULL_BEGIN [contactsSection addItem:loadingItem]; } } - + return @[ contactsSection ]; } __weak NewContactThreadViewController *weakSelf = self; @@ -650,25 +670,12 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)filteredSignalAccounts { - NSString *searchString = self.searchBar.text; - - ContactsViewHelper *helper = self.contactsViewHelper; - return [helper signalAccountsMatchingSearchString:searchString]; + return self.searchResults.signalAccounts; } - (NSArray *)filteredGroupThreads { - 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; - [groupThreads addObject:groupThread]; - }]; - - return [self.conversationSearcher filterGroupThreads:groupThreads withSearchText:self.searchBar.text]; + return self.searchResults.groupThreads; } #pragma mark - No Contacts Mode @@ -683,14 +690,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)presentInviteFlow { OWSInviteFlow *inviteFlow = - [[OWSInviteFlow alloc] initWithPresentingViewController:self - contactsManager:self.contactsViewHelper.contactsManager]; + [[OWSInviteFlow alloc] initWithPresentingViewController:self contactsManager:self.contactsManager]; [self presentViewController:inviteFlow.actionSheetController animated:YES completion:nil]; } - (void)showContactAppropriateViews { - if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized) { + if (self.contactsManager.isSystemContactsAuthorized) { if (self.contactsViewHelper.hasUpdatedContactsAtLeastOnce && self.contactsViewHelper.signalAccounts.count < 1 && ![Environment.shared.preferences hasDeclinedNoContactsView]) { self.isNoContactsModeActive = YES; @@ -733,8 +739,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSInviteFlow *inviteFlow = - [[OWSInviteFlow alloc] initWithPresentingViewController:self - contactsManager:self.contactsViewHelper.contactsManager]; + [[OWSInviteFlow alloc] initWithPresentingViewController:self contactsManager:self.contactsManager]; OWSAssertDebug([phoneNumber length] > 0); NSString *confirmMessage = NSLocalizedString(@"SEND_SMS_CONFIRM_TITLE", @""); @@ -866,6 +871,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { + [BenchManager startEventWithTitle:@"Compose Search" eventId:@"Compose Search"]; [self searchTextDidChange]; } @@ -891,11 +897,22 @@ NS_ASSUME_NONNULL_BEGIN - (void)searchTextDidChange { - [self updateSearchPhoneNumbers]; - - [self updateTableContents]; + NSString *searchText = self.searchText; + [self.uiDatabaseConnection + asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + self.searchResults = [self.conversationSearcher searchForComposeScreenWithSearchText:searchText + transaction:transaction + contactsManager:self.contactsManager]; + } + completionBlock:^{ + [self updateSearchPhoneNumbers]; + [self updateTableContents]; + [BenchManager completeEventWithEventId:@"Compose Search"]; + }]; } +#pragma mark - + - (NSDictionary *)callingCodesToCountryCodeMap { static NSDictionary *result = nil; @@ -928,9 +945,20 @@ NS_ASSUME_NONNULL_BEGIN return nil; } +- (NSString *)searchText +{ + NSString *rawText = self.searchBar.text; + NSString *stripped = rawText.ows_stripped; + if (stripped.length == 0) { + return @""; + } else { + return stripped; + } +} + - (NSArray *)parsePossibleSearchPhoneNumbers { - NSString *searchText = self.searchBar.text; + NSString *searchText = self.searchText; if (searchText.length < 8) { return @[]; diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index 315d5b5e6..5f30304a0 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -51,7 +51,8 @@ public class ConversationSearchResult: Comparable where SortKey: Compar } } -public class ContactSearchResult: Comparable { +@objc +public class ContactSearchResult: NSObject, Comparable { public let signalAccount: SignalAccount public let contactsManager: ContactsManagerProtocol @@ -77,7 +78,8 @@ public class ContactSearchResult: Comparable { } } -public class SearchResultSet { +@objc +public class SearchResultSet: NSObject { public let searchText: String public let conversations: [ConversationSearchResult] public let contacts: [ContactSearchResult] @@ -99,6 +101,67 @@ public class SearchResultSet { } } +@objc +public class GroupSearchResult: NSObject, Comparable { + public let thread: ThreadViewModel + + private let sortKey: ConversationSortKey + + init(thread: ThreadViewModel, sortKey: ConversationSortKey) { + self.thread = thread + self.sortKey = sortKey + } + + // MARK: Comparable + + public static func < (lhs: GroupSearchResult, rhs: GroupSearchResult) -> Bool { + return lhs.sortKey < rhs.sortKey + } + + // MARK: Equatable + + public static func == (lhs: GroupSearchResult, rhs: GroupSearchResult) -> Bool { + return lhs.thread.threadRecord.uniqueId == rhs.thread.threadRecord.uniqueId + } +} + +@objc +public class ComposeScreenSearchResultSet: NSObject { + + @objc + public let searchText: String + + @objc + public let groups: [GroupSearchResult] + + @objc + public var groupThreads: [TSGroupThread] { + return groups.compactMap { $0.thread.threadRecord as? TSGroupThread } + } + + @objc + public let signalContacts: [ContactSearchResult] + + @objc + public var signalAccounts: [SignalAccount] { + return signalContacts.map { $0.signalAccount } + } + + public init(searchText: String, groups: [GroupSearchResult], signalContacts: [ContactSearchResult]) { + self.searchText = searchText + self.groups = groups + self.signalContacts = signalContacts + } + + @objc + public static let empty = ComposeScreenSearchResultSet(searchText: "", groups: [], signalContacts: []) + + @objc + public var isEmpty: Bool { + return groups.isEmpty && signalContacts.isEmpty + } +} + @objc public class ConversationSearcher: NSObject { @@ -119,6 +182,48 @@ public class ConversationSearcher: NSObject { super.init() } + @objc + public func searchForComposeScreen(searchText: String, + transaction: YapDatabaseReadTransaction, + contactsManager: ContactsManagerProtocol) -> ComposeScreenSearchResultSet { + + var signalContacts: [ContactSearchResult] = [] + var groups: [GroupSearchResult] = [] + + self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in + + switch match { + case let signalAccount as SignalAccount: + let searchResult = ContactSearchResult(signalAccount: signalAccount, contactsManager: contactsManager) + signalContacts.append(searchResult) + case let groupThread as TSGroupThread: + let sortKey = ConversationSortKey(creationDate: groupThread.creationDate, + lastMessageReceivedAtDate: groupThread.lastInteractionForInbox(transaction: transaction)?.receivedAtDate()) + let threadViewModel = ThreadViewModel(thread: groupThread, transaction: transaction) + let searchResult = GroupSearchResult(thread: threadViewModel, sortKey: sortKey) + groups.append(searchResult) + case is TSContactThread: + // not included in compose screen results + break + case is TSMessage: + // not included in compose screen results + break + default: + owsFailDebug("unhandled item: \(match)") + } + } + + // Order "contact results by display name. + signalContacts.sort() + + // Order the conversation and message results in reverse chronological order. + // The contact results are pre-sorted by display name. + groups.sort(by: >) + + return ComposeScreenSearchResultSet(searchText: searchText, groups: groups, signalContacts: signalContacts) + } + + @objc public func results(searchText: String, transaction: YapDatabaseReadTransaction, contactsManager: ContactsManagerProtocol) -> SearchResultSet { @@ -223,7 +328,7 @@ public class ConversationSearcher: NSObject { let groupName = groupThread.groupModel.groupName let memberStrings = groupThread.groupModel.groupMemberIds.map { recipientId in self.indexingString(recipientId: recipientId) - }.joined(separator: " ") + }.joined(separator: " ") return "\(memberStrings) \(groupName ?? "")" }