// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "SelectThreadViewController.h" #import "BlockListUIUtils.h" #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" #import "NSString+OWS.h" #import "OWSContactsManager.h" #import "OWSTableViewController.h" #import "ThreadViewHelper.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN @interface SelectThreadViewController () @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @property (nonatomic, readonly) ConversationSearcher *conversationSearcher; @property (nonatomic, readonly) ThreadViewHelper *threadViewHelper; @property (nonatomic, readonly) OWSTableViewController *tableViewController; @property (nonatomic, readonly) UISearchBar *searchBar; @end #pragma mark - @implementation SelectThreadViewController - (void)loadView { [super loadView]; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop target:self action:@selector(dismissPressed:)]; self.view.backgroundColor = [UIColor whiteColor]; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; _conversationSearcher = ConversationSearcher.shared; _threadViewHelper = [ThreadViewHelper new]; _threadViewHelper.delegate = self; [self createViews]; [self updateTableContents]; } - (void)viewDidLoad { [super viewDidLoad]; [self.navigationController.navigationBar setTranslucent:NO]; } - (void)createViews { OWSAssert(self.selectThreadViewDelegate); // Search UISearchBar *searchBar = [UISearchBar new]; _searchBar = searchBar; searchBar.searchBarStyle = UISearchBarStyleMinimal; searchBar.delegate = self; searchBar.placeholder = NSLocalizedString(@"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT", @""); searchBar.backgroundColor = [UIColor whiteColor]; [searchBar sizeToFit]; UIView *header = [self.selectThreadViewDelegate createHeaderWithSearchBar:searchBar]; if (!header) { header = searchBar; } [self.view addSubview:header]; [header autoPinWidthToSuperview]; [header autoPinToTopLayoutGuideOfViewController:self withInset:0]; [header setCompressionResistanceVerticalHigh]; [header setContentHuggingVerticalHigh]; // Table _tableViewController = [OWSTableViewController new]; _tableViewController.delegate = self; [self.view addSubview:self.tableViewController.view]; [_tableViewController.view autoPinWidthToSuperview]; [_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:header]; [self autoPinViewToBottomGuideOrKeyboard:self.tableViewController.view]; } #pragma mark - UISearchBarDelegate - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [self updateTableContents]; } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { [self updateTableContents]; } - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { [self updateTableContents]; } - (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar { [self updateTableContents]; } - (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope { [self updateTableContents]; } #pragma mark - Table Contents - (void)updateTableContents { __weak SelectThreadViewController *weakSelf = self; ContactsViewHelper *helper = self.contactsViewHelper; OWSTableContents *contents = [OWSTableContents new]; // 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]) { [recentChatsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ SelectThreadViewController *strongSelf = weakSelf; OWSCAssert(strongSelf); // To be consistent with the threads (above), we use ContactTableViewCell // instead of InboxTableViewCell to present contacts and threads. ContactTableViewCell *cell = [ContactTableViewCell new]; [cell configureWithThread:thread contactsManager:helper.contactsManager]; return cell; } customRowHeight:[ContactTableViewCell rowHeight] actionBlock:^{ [weakSelf.selectThreadViewDelegate threadWasSelected:thread]; }]]; } if (recentChatsSection.itemCount > 0) { [contents addSection:recentChatsSection]; } // 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) { [otherContactsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ SelectThreadViewController *strongSelf = weakSelf; OWSCAssert(strongSelf); ContactTableViewCell *cell = [ContactTableViewCell new]; BOOL isBlocked = [helper isRecipientIdBlocked:signalAccount.recipientId]; if (isBlocked) { cell.accessoryMessage = NSLocalizedString(@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked."); } else { OWSAssert(cell.accessoryMessage == nil); } [cell configureWithSignalAccount:signalAccount contactsManager:helper.contactsManager]; return cell; } customRowHeight:[ContactTableViewCell rowHeight] actionBlock:^{ [weakSelf signalAccountWasSelected:signalAccount]; }]]; } if (otherContactsSection.itemCount > 0) { [contents addSection:otherContactsSection]; } 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]; } self.tableViewController.contents = contents; } - (void)signalAccountWasSelected:(SignalAccount *)signalAccount { OWSAssert(signalAccount); OWSAssert(self.selectThreadViewDelegate); ContactsViewHelper *helper = self.contactsViewHelper; if ([helper isRecipientIdBlocked:signalAccount.recipientId] && ![self.selectThreadViewDelegate canSelectBlockedContact]) { __weak SelectThreadViewController *weakSelf = self; [BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount fromViewController:self blockingManager:helper.blockingManager contactsManager:helper.contactsManager completionBlock:^(BOOL isBlocked) { if (!isBlocked) { [weakSelf signalAccountWasSelected:signalAccount]; } }]; return; } __block TSThread *thread = nil; [[TSStorageManager sharedManager].dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { thread = [TSContactThread getOrCreateThreadWithContactId:signalAccount.recipientId transaction:transaction]; }]; OWSAssert(thread); [self.selectThreadViewDelegate threadWasSelected:thread]; } #pragma mark - Filter - (NSArray *)filteredThreadsWithSearchText { NSString *searchTerm = [[self.searchBar text] ows_stripped]; return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm]; } - (NSArray *)filteredSignalAccountsWithSearchText { // We don't want to show a 1:1 thread with Alice and Alice's contact, // so we de-duplicate by recipientId. NSArray *threads = self.threadViewHelper.threads; NSMutableSet *contactIdsToIgnore = [NSMutableSet new]; for (TSThread *thread in threads) { if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; [contactIdsToIgnore addObject:contactThread.contactIdentifier]; } } NSString *searchString = self.searchBar.text; NSArray *matchingAccounts = [self.contactsViewHelper signalAccountsMatchingSearchString:searchString]; return [matchingAccounts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(SignalAccount *signalAccount, NSDictionary *_Nullable bindings) { return ![contactIdsToIgnore containsObject:signalAccount.recipientId]; }]]; } #pragma mark - Events - (void)dismissPressed:(id)sender { [self.searchBar resignFirstResponder]; [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - OWSTableViewControllerDelegate - (void)tableViewWillBeginDragging { [self.searchBar resignFirstResponder]; } #pragma mark - ThreadViewHelperDelegate - (void)threadListDidChange { [self updateTableContents]; } #pragma mark - ContactsViewHelperDelegate - (void)contactsViewHelperDidUpdateContacts { [self updateTableContents]; } - (BOOL)shouldHideLocalNumber { return NO; } @end NS_ASSUME_NONNULL_END