element-ios/matrixConsole/ViewController/ContactsViewController.m

725 lines
26 KiB
Objective-C

/*
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"
#import "MatrixHandler.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;
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// 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;
[mxHandler 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;
[mxHandler 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