// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "ContactGroupPickerViewController.h" #import "ContactTableDataSource.h" #import "GroupTableDataSource.h" #import "RecentTableDataSource.h" #import "WorkContactTableDataSource.h" #import "BundleUtil.h" #import "RectUtil.h" #import "AppGroup.h" #import "LicenseStore.h" #import "PickerContactCell.h" #import "PickerGroupCell.h" #define LAST_SELECTED_MODE @"ContactGroupPickerLastSelectedMode" typedef enum : NSUInteger { ModeContact, ModeGroup, ModeRecent, ModeWorkContact } SelectionMode; @interface ContactGroupPickerViewController () @property SelectionMode mode; @property id currentDataSource; @property CGFloat searchBarHeight; @property BOOL isSearchBarHidden; @property BOOL isTextInputHidden; @end @implementation ContactGroupPickerViewController + (UIStoryboard *)contactPickerStoryboard { NSBundle *frameworkBundle = [BundleUtil frameworkBundle]; return [UIStoryboard storyboardWithName:@"ContactPicker" bundle:frameworkBundle]; } + (ModalNavigationController *)pickerFromStoryboardWithDelegate:(id)delegate { UIStoryboard *storyboard = [ContactGroupPickerViewController contactPickerStoryboard]; ModalNavigationController *navigationController = [storyboard instantiateInitialViewController]; navigationController.dismissOnTapOutside = NO; navigationController.modalDelegate = delegate; ContactGroupPickerViewController *picker = (ContactGroupPickerViewController *)[navigationController topViewController]; picker.delegate = delegate; picker.enableMulitSelection = YES; //defaults to YES picker.enableTextInput = YES; //defaults to YES return navigationController; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; } - (void)viewDidLoad { [super viewDidLoad]; WorkContactTableDataSource *workDataSource = [WorkContactTableDataSource workContactTableDataSource]; if ([LicenseStore requiresLicenseKey] && workDataSource.countOfWorkContacts > 0) { [self.segmentedControl insertSegmentWithTitle:[BundleUtil localizedStringForKey:@"work"] atIndex:ModeWorkContact animated:NO]; } NSUserDefaults *defaults = [AppGroup userDefaults]; NSNumber *type = [defaults objectForKey:LAST_SELECTED_MODE]; if (type) { _mode = type.integerValue; } else { _mode = ModeContact; } _sendAsFileSwitch.on = false; self.searchController = [[UISearchController alloc]initWithSearchResultsController:nil]; self.searchController.searchBar.showsScopeBar = NO; self.searchController.searchBar.scopeButtonTitles = nil; self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeNone; self.searchController.searchBar.delegate = self; self.searchController.searchResultsUpdater = self; self.searchController.searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; [self.searchController.searchBar sizeToFit]; self.searchController.searchBar.barStyle = UISearchBarStyleMinimal; self.searchController.dimsBackgroundDuringPresentation = NO; self.definesPresentationContext = NO; if (@available(iOS 13.0, *)) { // iOS 13 and 13.1 have a bug. When searchbar was active, the navigationitem is not available // Bug should be fixed in 13.2 if (@available(iOS 13.2, *)) { self.searchController.hidesNavigationBarDuringPresentation = false; } else { self.searchController.hidesNavigationBarDuringPresentation = true; } } else { self.searchController.hidesNavigationBarDuringPresentation = false; } if (@available(iOS 11.0, *)) { self.navigationItem.searchController = _searchController; self.navigationItem.hidesSearchBarWhenScrolling = NO; } _searchBarHeight = self.searchController.searchBar.frame.size.height; [self updateUIStrings]; if (_submitOnSelect) { _controlView.hidden = YES; } _isTextInputHidden = YES; _tableView.dataSource = _currentDataSource; _tableView.delegate = self; _tableView.allowsMultipleSelection = _enableMulitSelection; [self registerForKeyboardNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [self setupColors]; self.tableView.estimatedRowHeight = UITableViewAutomaticDimension; self.tableView.rowHeight = UITableViewAutomaticDimension; CGRect frame = CGRectZero; frame.size.height = CGFLOAT_MIN; [self.tableView setTableHeaderView:[[UIView alloc] initWithFrame:frame]]; } - (void)setupColors { [self.view setBackgroundColor:[Colors background]]; _controlView.backgroundColor = [Colors backgroundDark]; _buttonView.backgroundColor = [Colors backgroundDark]; [_sendButton setTintColor:[Colors fontLink]]; [_addTextButton setTintColor:[Colors fontLink]]; [_hideTextButton setTintColor:[Colors fontLink]]; [_textView setBackgroundColor:[Colors backgroundDark]]; [_textView setTextColor:[Colors fontNormal]]; [Colors updateTableView:self.tableView]; [Colors updateSearchBar:_searchController.searchBar]; [Colors updateKeyboardAppearanceFor:self.textView]; [_hairLineView setBackgroundColor:[Colors hairline]]; _sendAsFileLabel.textColor = [Colors fontLink]; [self.navigationItem.leftBarButtonItem setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: [Colors main], NSForegroundColorAttributeName, nil] forState:UIControlStateNormal]; [self.navigationItem.leftBarButtonItem setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: [Colors main], NSForegroundColorAttributeName, nil] forState:UIControlStateHighlighted]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.navigationItem.titleView = _segmentedControl; self.segmentedControl.selectedSegmentIndex = _mode; [self segmentedControlChanged:nil]; if (_renderType == nil) { _renderType = @0; } } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (_enableTextInput == NO) { _addTextButton.hidden = YES; } [self updateButtons]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; NSUserDefaults *defaults = [AppGroup userDefaults]; [defaults setValue:[NSNumber numberWithInteger:_mode] forKey:LAST_SELECTED_MODE]; } - (void)setEnableMulitSelection:(BOOL)allowMulitSelection { _enableMulitSelection = allowMulitSelection; _tableView.allowsMultipleSelection = _enableMulitSelection; } - (void)updateUIStrings { // iOS 10 and 12 have different subviews sorting, so we have to check it with name and replace it at the end with the image [self.segmentedControl setTitle:@"contacts" forSegmentAtIndex:ModeContact]; [self.segmentedControl setTitle:@"groups" forSegmentAtIndex:ModeGroup]; [self.segmentedControl setTitle:@"recent" forSegmentAtIndex:ModeRecent]; if ([LicenseStore requiresLicenseKey] && self.segmentedControl.numberOfSegments == 4) { [self.segmentedControl setTitle:@"work" forSegmentAtIndex:ModeWorkContact]; } for (int i = 0; i < self.segmentedControl.numberOfSegments; i++) { UIView *segment = self.segmentedControl.subviews[i]; for (id subview in segment.subviews) { if ([subview isKindOfClass:[UILabel class]]) { UILabel *label = (UILabel *)subview; if ([label.text isEqualToString:@"contacts"]) { segment.accessibilityLabel = [BundleUtil localizedStringForKey:@"contacts"]; } else if ([label.text isEqualToString:@"groups"]) { segment.accessibilityLabel = [BundleUtil localizedStringForKey:@"groups"]; } else if ([label.text isEqualToString:@"recent"]) { segment.accessibilityLabel = [BundleUtil localizedStringForKey:@"recent"]; } else if ([label.text isEqualToString:@"work"]) { segment.accessibilityLabel = [BundleUtil localizedStringForKey:@"work"]; } } } } [self.segmentedControl setTitle:nil forSegmentAtIndex:ModeContact]; [self.segmentedControl setTitle:nil forSegmentAtIndex:ModeGroup]; [self.segmentedControl setTitle:nil forSegmentAtIndex:ModeRecent]; [self.segmentedControl setImage:[BundleUtil imageNamed:@"Contact"] forSegmentAtIndex:ModeContact]; [self.segmentedControl setImage:[BundleUtil imageNamed:@"Group"] forSegmentAtIndex:ModeGroup]; [self.segmentedControl setImage:[BundleUtil imageNamed:@"Recent"] forSegmentAtIndex:ModeRecent]; if ([LicenseStore requiresLicenseKey] && self.segmentedControl.numberOfSegments == 4) { [self.segmentedControl setTitle:nil forSegmentAtIndex:ModeWorkContact]; [self.segmentedControl setImage:[BundleUtil imageNamed:@"Case"] forSegmentAtIndex:ModeWorkContact]; } [_addTextButton setTitle:[BundleUtil localizedStringForKey:@"addText"] forState:UIControlStateNormal]; [_hideTextButton setTitle:[BundleUtil localizedStringForKey:@"hide"] forState:UIControlStateNormal]; [_sendButton setTitle:[BundleUtil localizedStringForKey:@"send"]]; [_sendAsFileLabel setText:[BundleUtil localizedStringForKey:@"send_as_file"]]; } - (BOOL)shouldAutorotate { return YES; } -(UIInterfaceOrientationMask)supportedInterfaceOrientations { if (SYSTEM_IS_IPAD) { return UIInterfaceOrientationMaskAll; } return UIInterfaceOrientationMaskAllButUpsideDown; } # pragma mark - Keyboard Notifications - (void)registerForKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } - (void)keyboardWillShow:(NSNotification *)notification { [self processKeyboardNotification:notification willHide:NO]; } - (void)keyboardWillHide:(NSNotification *)notification { [self processKeyboardNotification:notification willHide:YES]; } - (void)processKeyboardNotification:(NSNotification*)notification willHide:(BOOL)willHide { NSDictionary* info = [notification userInfo]; NSNumber *durationValue = info[UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration = durationValue.doubleValue; NSNumber *curveValue = info[UIKeyboardAnimationCurveUserInfoKey]; UIViewAnimationCurve animationCurve = curveValue.intValue; CGRect keyboardRect = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat keyboardHeight = willHide ? 0.0f : keyboardRect.size.height; [UIView animateWithDuration:animationDuration delay:0 options:(animationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState) animations:^{ CGFloat offset = 0.0; if (_isTextInputHidden == true) { offset = willHide == true ? 50.0 : keyboardHeight + _buttonView.frame.size.height; } else { offset = willHide == true ? 50.0 : keyboardHeight + _controlView.frame.size.height; } if (@available(iOS 11.0, *)) { float difference = self.view.safeAreaLayoutGuide.layoutFrame.size.height - self.view.frame.size.height; if (willHide == false) { offset += difference; } } _tableViewBottomConstraint.constant = offset; } completion:^(BOOL finished) { }]; } #pragma mark - UIApplication Notifications - (void)willResignActive:(NSNotification *)notification { [self hideTextAction:nil]; } #pragma mark - table view delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } -(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } - (void)tableView:(UITableView *)tableView willDisplayHeaderView:(nonnull UIView *)view forSection:(NSInteger)section { UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView*)view; [headerView.contentView setBackgroundColor:[Colors backgroundDark]]; [headerView.textLabel setTextColor:[Colors fontNormal]]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[UITableViewCell class]]) { [Colors updateTableViewCell:cell]; UIView *selectedBgView = [[UIView alloc] init]; selectedBgView.backgroundColor = [Colors shareExtensionSelectedBackground]; [cell setSelectedBackgroundView:selectedBgView]; } if ([cell isKindOfClass:[PickerContactCell class]]) { PickerContactCell *pickerContactCell = (PickerContactCell *)cell; BOOL found = false; for (Conversation *conversation in [_currentDataSource selectedConversations]) { if (conversation.contact != nil && conversation.contact == pickerContactCell.contact) { found = true; } } if (found == true) { [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; } } if ([cell isKindOfClass:[PickerGroupCell class]]) { PickerGroupCell *pickerGroupCell = (PickerGroupCell *)cell; BOOL found = false; for (Conversation *conversation in [_currentDataSource selectedConversations]) { if (conversation.groupId != nil && [conversation.groupId isEqualToData:pickerGroupCell.group.groupId]) { found = true; } } if (found == true) { [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; } } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [_currentDataSource selectedCellAtIndexPath:indexPath selected:YES]; if (_submitOnSelect) { [self.delegate contactPicker:self didPickConversations:_currentDataSource.selectedConversations renderType:_renderType sendAsFile:_sendAsFileSwitch.on]; } else { [self updateButtons]; } } - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { [_currentDataSource selectedCellAtIndexPath:indexPath selected:NO]; [self updateButtons]; } - (void)updateButtons { NSUInteger count = [_currentDataSource selectedConversations].count; BOOL hasSelection = count > 0; _sendButton.enabled = hasSelection; if (hasSelection) { [_sendButton setTitle:[NSString stringWithFormat:@"%@ (%lu)", [BundleUtil localizedStringForKey:@"send"], (unsigned long)count]]; } else { [_sendButton setTitle:[BundleUtil localizedStringForKey:@"send"]]; } } - (void)hideSearchBar:(BOOL)hide { if (@available(iOS 11.0, *)) { if (_isSearchBarHidden == hide) { return; } _isSearchBarHidden = hide; [UIView animateWithDuration:0.3 animations:^{ [self.searchController.searchBar setHidden:hide]; }]; } } - (void)hideTextInput:(BOOL)hide { if (_isTextInputHidden == hide) { return; } if (hide) { [self updateAddButtonTitle]; } _isTextInputHidden = hide; [UIView animateWithDuration:0.3 animations:^{ _hideTextButton.hidden = hide; _addTextButton.hidden = !hide; _textView.hidden = hide; }]; } - (void)updateAddButtonTitle { NSString *addButtonTitle; if ([self hasAdditionalText]) { _addTextButton.frame = [RectUtil setWidthOf:_addTextButton.frame width:150.0]; addButtonTitle = [self trimmedText]; } else { addButtonTitle = [BundleUtil localizedStringForKey:@"addText"]; } [_addTextButton setTitle:addButtonTitle forState:UIControlStateNormal]; } - (NSString *)additionalTextToSend { if ([self hasAdditionalText]) { return [self trimmedText]; } else { return nil; } } - (BOOL)hasAdditionalText { return [self trimmedText].length > 0; } - (NSString *)trimmedText { return [_textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [_textView resignFirstResponder]; [_searchController.searchBar resignFirstResponder]; [self hideTextInput:YES]; } #pragma mark - Actions - (IBAction)addTextAction:(id)sender { [self hideTextInput:NO]; if (@available(iOS 11.0, *)) { } else { [self hideSearchBar:YES]; } [_textView becomeFirstResponder]; _controlView.backgroundColor = [Colors background]; _textView.backgroundColor = [Colors background]; } - (IBAction)hideTextAction:(id)sender { [self hideTextInput:YES]; if (_mode != ModeRecent) { [self hideSearchBar:NO]; } [_textView resignFirstResponder]; _controlView.backgroundColor = _buttonView.backgroundColor; _textView.backgroundColor = _buttonView.backgroundColor; } - (IBAction)cancelAction:(id)sender { [self.delegate contactPickerDidCancel:self]; } - (IBAction)doneAction:(id)sender { [self.searchController setActive:false]; [self.delegate contactPicker:self didPickConversations:_currentDataSource.selectedConversations renderType:_renderType sendAsFile:_sendAsFileSwitch.on]; } - (IBAction)segmentedControlChanged:(id)sender { _mode = self.segmentedControl.selectedSegmentIndex; [_textView resignFirstResponder]; [self hideTextInput:YES]; switch (_mode) { case ModeContact: [self hideSearchBar:NO]; _currentDataSource = [ContactTableDataSource contactTableDataSource]; [_currentDataSource filterByWords: [self searchWordsForText:_searchController.searchBar.text]]; break; case ModeGroup: [self hideSearchBar:NO]; _currentDataSource = [GroupTableDataSource groupTableDataSource]; [_currentDataSource filterByWords: [self searchWordsForText:_searchController.searchBar.text]]; break; case ModeRecent: [self hideSearchBar:YES]; _currentDataSource = [RecentTableDataSource recentTableDataSource]; break; case ModeWorkContact: [self hideSearchBar:NO]; _currentDataSource = [WorkContactTableDataSource workContactTableDataSource]; [_currentDataSource filterByWords: [self searchWordsForText:_searchController.searchBar.text]]; break; default: break; } [self updateButtons]; _tableView.dataSource = _currentDataSource; [self.tableView reloadData]; } #pragma mark - Scroll view delegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [_searchController.searchBar resignFirstResponder]; [_textView resignFirstResponder]; [self hideTextInput:YES]; if (_mode != ModeRecent) { [self hideSearchBar:NO]; } } #pragma mark - Search bar delegate - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { NSArray *searchWords = [self searchWordsForText:searchText]; [_currentDataSource filterByWords: searchWords]; [self.tableView reloadData]; } -(void)updateSearchResultsForSearchController:(UISearchController *)searchController { NSArray *searchWords = [self searchWordsForText:_searchController.searchBar.text]; [_currentDataSource filterByWords: searchWords]; [self.tableView reloadData]; } - (NSArray *)searchWordsForText:(NSString *)text { NSArray *searchWords = nil; if (text && [text length] > 0) { searchWords = [text componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } return searchWords; } @end