/* Copyright 2015 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #import "ContactsViewController.h" // application info #import "AppDelegate.h" // contacts management #import "ContactManager.h" #import "MXCContact.h" #import "MXCEmail.h" #import "MXCPhoneNumber.h" // contact cell #import "ContactTableCell.h" #import "RageShakeManager.h" // #import "ContactDetailsViewController.h" NSString *const kInvitationMessage = @"I'd like to chat with you with matrix. Please, visit the website http://matrix.org to have more information."; @interface ContactsViewController () { // YES -> only matrix users // NO -> display local contacts BOOL displayMatrixUsers; // screenshot of the local contacts NSMutableArray* localContacts; SectionedContacts* sectionedLocalContacts; // screenshot of the matrix users NSMutableDictionary* matrixUserByMatrixID; SectionedContacts* sectionedMatrixContacts; // tap on thumbnail to display contact info MXCContact* selectedContact; // Search UISearchBar *contactsSearchBar; NSMutableArray *filteredContacts; SectionedContacts* sectionedFilteredContacts; BOOL searchBarShouldEndEditing; NSString* latestSearchedPattern; } @property (strong, nonatomic) MXKAlert *startChatMenu; @property (strong, nonatomic) MXKAlert *allowContactSyncAlert; @property (weak, nonatomic) IBOutlet UISegmentedControl* contactsControls; @end @implementation ContactsViewController - (void)viewDidLoad { [super viewDidLoad]; // get the system collation titles collationTitles = [[UILocalizedIndexedCollation currentCollation]sectionTitles]; // global init displayMatrixUsers = (0 == self.contactsControls.selectedSegmentIndex); matrixUserByMatrixID = [[NSMutableDictionary alloc] init]; // add the search icon on the right // need to add more buttons ? UIBarButtonItem *searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)]; self.navigationItem.rightBarButtonItems = @[searchButton]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactsRefresh:) name:kContactManagerContactsListRefreshNotification object:nil]; // Set rageShake handler self.rageShakeManager = [RageShakeManager sharedManager]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // Leave potential search session if (contactsSearchBar) { [self searchBarCancelButtonClicked:contactsSearchBar]; } } - (void)scrollToTop { // stop any scrolling effect [UIView setAnimationsEnabled:NO]; // before scrolling to the tableview top self.tableView.contentOffset = CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top); [UIView setAnimationsEnabled:YES]; } // should be called when resetting the application // the contact manager warn there is a contacts list update // but the Matrix SDK handler has no more userID -> so assume there is a reset - (void)reset { // Leave potential search session if (contactsSearchBar) { [self searchBarCancelButtonClicked:contactsSearchBar]; } localContacts = nil; sectionedLocalContacts = nil; matrixUserByMatrixID = [[NSMutableDictionary alloc] init];; sectionedMatrixContacts = nil; [self.contactsControls setSelectedSegmentIndex:0]; [self.tableView reloadData]; } - (void)refreshMatrixUsers { if (displayMatrixUsers) { if (contactsSearchBar) { [self updateSectionedMatrixContacts]; latestSearchedPattern = nil; [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; } else { [self.tableView reloadData]; } } } #pragma mark - overridden MXKTableViewController methods - (void)setMxSession:(MXSession *)session { [super setMxSession:session]; [self refreshMatrixUsers]; } - (void)didMatrixSessionStateChange { [super didMatrixSessionStateChange]; [self refreshMatrixUsers]; } #pragma mark - UITableView delegate - (void)updateSectionedLocalContacts { [self stopActivityIndicator]; ContactManager* sharedManager = [ContactManager sharedManager]; if (!localContacts) { localContacts = sharedManager.contacts; } if (!sectionedLocalContacts) { sectionedLocalContacts = [sharedManager getSectionedContacts:sharedManager.contacts]; } } - (void)updateSectionedMatrixContacts { // Check whether mxSession is available if (!self.mxSession) { [self startActivityIndicator]; sectionedMatrixContacts = nil; } else { [self stopActivityIndicator]; NSArray* usersIDs = [self oneToOneRoomMemberIDs]; // return a MatrixIDs list of 1:1 room members NSMutableArray* knownUserIDs = [[matrixUserByMatrixID allKeys] mutableCopy]; // list the contacts IDs // avoid delete and create the same ones // it could save thumbnail downloads for(NSString* userID in usersIDs) { // MXUser* user = [self.mxSession userWithUserId:userID]; // sanity check if (user) { // managed UserID [knownUserIDs removeObject:userID]; MXCContact* contact = [matrixUserByMatrixID objectForKey:userID]; // already defined if (contact) { contact.displayName = (user.displayname.length > 0) ? user.displayname : user.userId; } else { contact = [[MXCContact alloc] initWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) matrixID:user.userId]; [matrixUserByMatrixID setValue:contact forKey:userID]; } } } // some userIDs don't exist anymore for (NSString* userID in knownUserIDs) { [matrixUserByMatrixID removeObjectForKey:userID]; } sectionedMatrixContacts = [[ContactManager sharedManager] getSectionedContacts:[matrixUserByMatrixID allValues]]; } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // search in progress if (contactsSearchBar) { return sectionedFilteredContacts.sectionedContacts.count; } else if (displayMatrixUsers) { [self updateSectionedMatrixContacts]; return sectionedMatrixContacts.sectionedContacts.count; } else { [self updateSectionedLocalContacts]; return sectionedLocalContacts.sectionedContacts.count; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); return [[sectionedContacts.sectionedContacts objectAtIndex:section] count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 50; } - (NSString *)tableView:(UITableView *)aTableView titleForHeaderInSection:(NSInteger)section { if (contactsSearchBar) { // Hide section titles during search session return nil; } SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); if (sectionedContacts.sectionTitles.count <= section) { return nil; } else { return (NSString*)[sectionedContacts.sectionTitles objectAtIndex:section]; } } - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)aTableView { // do not display the collation during a search if (contactsSearchBar) { return nil; } else { [self.tableView setSectionIndexColor:[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor]; [self.tableView setSectionIndexBackgroundColor:[UIColor clearColor]]; return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; } } - (NSInteger)tableView:(UITableView *)aTableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); NSUInteger section = [sectionedContacts.sectionTitles indexOfObject:title]; // undefined title -> jump to the first valid non empty section if (NSNotFound == section) { NSUInteger systemCollationIndex = [collationTitles indexOfObject:title]; // find in the system collation if (NSNotFound != systemCollationIndex) { systemCollationIndex--; while ((systemCollationIndex == 0) && (NSNotFound == section)) { NSString* systemTitle = [collationTitles objectAtIndex:systemCollationIndex]; section = [sectionedContacts.sectionTitles indexOfObject:systemTitle]; systemCollationIndex--; } } } return section; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { // In case of search, the section titles are hidden and the search bar is displayed in first section header. if (contactsSearchBar) { if (section == 0) { return contactsSearchBar.frame.size.height; } return 0; } // Default section header height return 22; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { if (contactsSearchBar && section == 0) { return contactsSearchBar; } return nil; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { ContactTableCell* cell = [tableView dequeueReusableCellWithIdentifier:@"ContactCell" forIndexPath:indexPath]; SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); MXCContact* contact = nil; if (indexPath.section < sectionedContacts.sectionedContacts.count) { NSArray *thisSection = [sectionedContacts.sectionedContacts objectAtIndex:indexPath.section]; if (indexPath.row < thisSection.count) { contact = [thisSection objectAtIndex:indexPath.row]; } } // tap on matrix user thumbnail -> open a detailled sheet UITapGestureRecognizer* tapGesture = nil; // check if it is already defined // gesture in storyboard does not seem to work properly // it always triggers a tap event on the first cell for (UIGestureRecognizer* gesture in cell.thumbnailView.gestureRecognizers) { if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) { tapGesture = (UITapGestureRecognizer*)gesture; break; } } // add it if it is not yet defined if (!tapGesture) { UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onContactThumbnailTap:)]; [tap setNumberOfTouchesRequired:1]; [tap setNumberOfTapsRequired:1]; [tap setDelegate:self]; [cell.thumbnailView addGestureRecognizer:tap]; } cell.contact = contact; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; SectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts); MXCContact* contact = nil; if (indexPath.section < sectionedContacts.sectionedContacts.count) { NSArray *thisSection = [sectionedContacts.sectionedContacts objectAtIndex:indexPath.section]; if (indexPath.row < thisSection.count) { contact = [thisSection objectAtIndex:indexPath.row]; } } __weak typeof(self) weakSelf = self; NSArray* matrixIDs = contact.matrixIdentifiers; // matrix user ? if (matrixIDs.count) { // display only if the mxSession is available in matrix SDK handler if (self.mxSession) { // only 1 matrix ID if (matrixIDs.count == 1) { NSString* matrixID = [matrixIDs objectAtIndex:0]; self.startChatMenu = [[MXKAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with %@", matrixID] message:nil style:MXKAlertStyleAlert]; [self.startChatMenu addActionWithTitle:@"Cancel" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; }]; [self.startChatMenu addActionWithTitle:@"OK" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; [[AppDelegate theDelegate] startPrivateOneToOneRoomWithUserId:matrixID]; }]; } else { self.startChatMenu = [[MXKAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with "] message:nil style:MXKAlertStyleActionSheet]; for(NSString* matrixID in matrixIDs) { [self.startChatMenu addActionWithTitle:matrixID style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; [[AppDelegate theDelegate] startPrivateOneToOneRoomWithUserId:matrixID]; }]; } [self.startChatMenu addActionWithTitle:@"Cancel" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; }]; UIView *sourceView = [tableView cellForRowAtIndexPath:indexPath]; self.startChatMenu.sourceView = sourceView ? sourceView : tableView; } [self.startChatMenu showInViewController:self]; } } else { // invite to use matrix if (([MFMessageComposeViewController canSendText] ? contact.emailAddresses.count : 0) + (contact.phoneNumbers.count > 0)) { self.startChatMenu = [[MXKAlert alloc] initWithTitle:[NSString stringWithFormat:@"Invite this user to use matrix with"] message:nil style:MXKAlertStyleActionSheet]; // check if the target can send SMSes if ([MFMessageComposeViewController canSendText]) { // list phonenumbers for(MXCPhoneNumber* phonenumber in contact.phoneNumbers) { [self.startChatMenu addActionWithTitle:phonenumber.textNumber style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; // launch SMS composer MFMessageComposeViewController *messageComposer = [[MFMessageComposeViewController alloc] init]; if (messageComposer) { messageComposer.messageComposeDelegate = weakSelf; messageComposer.body =kInvitationMessage; messageComposer.recipients = [NSArray arrayWithObject:phonenumber.textNumber]; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf presentViewController:messageComposer animated:YES completion:nil]; }); } }]; } } // list emails for(MXCEmail* email in contact.emailAddresses) { [self.startChatMenu addActionWithTitle:email.emailAddress style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; dispatch_async(dispatch_get_main_queue(), ^{ NSString* subject = [ @"Matrix.org is magic" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSString* body = [kInvitationMessage stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"mailto:%@?subject=%@&body=%@", email.emailAddress, subject, body]]]; }); }]; } [self.startChatMenu addActionWithTitle:@"Cancel" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.startChatMenu = nil; }]; UIView *sourceView = [tableView cellForRowAtIndexPath:indexPath]; self.startChatMenu.sourceView = sourceView ? sourceView : tableView; [self.startChatMenu showInViewController:self]; } } } #pragma mark - Actions - (void)onContactsRefresh:(NSNotification *)notif { localContacts = nil; sectionedLocalContacts = nil; // there is an user id if (self.mxSession && self.mxSession.myUser.userId) { [self updateSectionedLocalContacts]; // if (!displayMatrixUsers) { if (contactsSearchBar) { latestSearchedPattern = nil; [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; } else { [self.tableView reloadData]; } } } else { // the client could have been logged out [self reset]; } } - (IBAction)onSegmentValueChange:(id)sender { if (sender == self.contactsControls) { displayMatrixUsers = (0 == self.contactsControls.selectedSegmentIndex); if (contactsSearchBar) { if (displayMatrixUsers) { [self updateSectionedMatrixContacts]; } else { [self updateSectionedLocalContacts]; } latestSearchedPattern = nil; [self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text]; } else { [self.tableView reloadData]; } if (!displayMatrixUsers) { MXKAppSettings* appSettings = [MXKAppSettings standardAppSettings]; if (!appSettings.syncLocalContacts) { __weak typeof(self) weakSelf = self; self.allowContactSyncAlert = [[MXKAlert alloc] initWithTitle:@"Allow local contacts synchronization ?" message:nil style:MXKAlertStyleAlert]; [self.allowContactSyncAlert addActionWithTitle:@"No" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.allowContactSyncAlert = nil; }]; [self.allowContactSyncAlert addActionWithTitle:@"Yes" style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { weakSelf.allowContactSyncAlert = nil; dispatch_async(dispatch_get_main_queue(), ^{ appSettings.syncLocalContacts = YES; [weakSelf.tableView reloadData]; }); }]; [self.allowContactSyncAlert showInViewController:self]; } } } } - (IBAction)onContactThumbnailTap:(id)sender { if ([sender isKindOfClass:[UITapGestureRecognizer class]]) { UIView* tappedView = ((UITapGestureRecognizer*)sender).view; // search the parentce cell while (tappedView && ![tappedView isKindOfClass:[ContactTableCell class]]) { tappedView = tappedView.superview; } // find it ? if ([tappedView isKindOfClass:[ContactTableCell class]]) { MXCContact* contact = ((ContactTableCell*)tappedView).contact; // open detailled sheet if there if (contact.matrixIdentifiers.count > 0) { selectedContact = ((ContactTableCell*)tappedView).contact; [self performSegueWithIdentifier:@"showContactDetails" sender:self]; } } } } #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"showContactDetails"]) { ContactDetailsViewController *contactDetailsViewController = segue.destinationViewController; contactDetailsViewController.contact = selectedContact; selectedContact = nil; } } #pragma mark MFMessageComposeViewControllerDelegate - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark Search management - (void)search:(id)sender { if (!contactsSearchBar) { SectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts; // Check whether there are data in which search if (sectionedContacts.sectionedContacts.count > 0) { // Create search bar contactsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)]; contactsSearchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; contactsSearchBar.showsCancelButton = YES; contactsSearchBar.returnKeyType = UIReturnKeyDone; contactsSearchBar.delegate = self; contactsSearchBar.tintColor = [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor; searchBarShouldEndEditing = NO; // init the table content latestSearchedPattern = @""; filteredContacts = [(displayMatrixUsers ? [matrixUserByMatrixID allValues] : localContacts) mutableCopy]; sectionedFilteredContacts = [[ContactManager sharedManager] getSectionedContacts:filteredContacts]; [self.tableView reloadData]; dispatch_async(dispatch_get_main_queue(), ^{ [contactsSearchBar becomeFirstResponder]; }); } } else { [self searchBarCancelButtonClicked:contactsSearchBar]; } } #pragma mark - UISearchBarDelegate - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar { searchBarShouldEndEditing = NO; return YES; } - (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar { return searchBarShouldEndEditing; } - (NSArray*)patternsFromText:(NSString*)text { NSArray* items = [text componentsSeparatedByString:@" "]; if (items.count <= 1) { return items; } NSMutableArray* patterns = [[NSMutableArray alloc] init]; for (NSString* item in items) { if (item.length > 0) { [patterns addObject:item]; } } return patterns; } - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { if ((contactsSearchBar == searchBar) && (![latestSearchedPattern isEqualToString:searchText])) { latestSearchedPattern = searchText; // contacts NSArray* contacts = displayMatrixUsers ? [matrixUserByMatrixID allValues] : localContacts; // Update filtered list if (searchText.length && contacts.count) { filteredContacts = [[NSMutableArray alloc] init]; NSArray* patterns = [self patternsFromText:searchText]; for(MXCContact* contact in contacts) { if ([contact matchedWithPatterns:patterns]) { [filteredContacts addObject:contact]; } } } else { filteredContacts = [contacts mutableCopy]; } sectionedFilteredContacts = [[ContactManager sharedManager] getSectionedContacts:filteredContacts]; // Refresh display [self.tableView reloadData]; [self scrollToTop]; } } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { if (contactsSearchBar == searchBar) { // "Done" key has been pressed searchBarShouldEndEditing = YES; [contactsSearchBar resignFirstResponder]; } } - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { if (contactsSearchBar == searchBar) { // Leave search searchBarShouldEndEditing = YES; [contactsSearchBar resignFirstResponder]; [contactsSearchBar removeFromSuperview]; contactsSearchBar = nil; filteredContacts = nil; sectionedFilteredContacts = nil; latestSearchedPattern = nil; [self.tableView reloadData]; [self scrollToTop]; } } #pragma mark - Matrix session handling // return a MatrixIDs list of 1:1 room members - (NSArray*)oneToOneRoomMemberIDs { NSMutableArray* matrixIDs = [[NSMutableArray alloc] init]; if (self.mxSession) { for (MXRoom *mxRoom in self.mxSession.rooms) { NSArray* membersList = [mxRoom.state members]; // keep only 1:1 chat if ([mxRoom.state members].count <= 2) { for (MXRoomMember* member in membersList) { // not myself if (![member.userId isEqualToString:self.mxSession.myUser.userId]) { if ([matrixIDs indexOfObject:member.userId] == NSNotFound) { [matrixIDs addObject:member.userId]; } } } } } } return matrixIDs; } @end