Moved code under a subdirectory: Console

This commit is contained in:
manuroe
2015-04-03 09:04:22 +02:00
parent 1b24cdb7f1
commit 2b0350d14f
160 changed files with 0 additions and 0 deletions
@@ -0,0 +1,24 @@
/*
Copyright 2014 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 "MXCViewController.h"
#import "AuthInputsView.h"
@interface AuthenticationViewController : MXCViewController <UITextFieldDelegate, AuthInputsViewDelegate>
@end
@@ -0,0 +1,574 @@
/*
Copyright 2014 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 "AuthenticationViewController.h"
#import "MatrixSDKHandler.h"
#import "AppDelegate.h"
#import "MXCAlert.h"
#import "MXCRegistrationWebView.h"
@interface AuthenticationViewController () {
// Current request in progress
MXHTTPOperation *mxCurrentOperation;
// Array of flows supported by the home server and implemented by the app (for the current auth type)
NSMutableArray *supportedFlows;
// The current view in which auth inputs are displayed
AuthInputsView *currentAuthInputsView;
// reference to any opened alert view
MXCAlert *alert;
}
// Return true if the provided flow (kMXLoginFlowType) is supported by the application
+ (BOOL)isImplementedFlowType:(NSString*)flowType forAuthType:(AuthenticationType)authType;
// The current authentication type
@property (nonatomic) AuthenticationType authType;
@property (nonatomic) MXLoginFlow *selectedFlow;
@property (strong, nonatomic) IBOutlet UIScrollView *authenticationScrollView;
@property (weak, nonatomic) IBOutlet UIView *contentView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewHeightConstraint;
@property (weak, nonatomic) IBOutlet UILabel *createAccountLabel;
@property (weak, nonatomic) IBOutlet UIView *authInputsContainerView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *authInputContainerViewHeightConstraint;
@property (weak, nonatomic) IBOutlet AuthInputsPasswordBasedView *authInputsPasswordBasedView;
@property (weak, nonatomic) IBOutlet AuthInputsEmailCodeBasedView *authInputsEmailCodeBasedView;
@property (weak, nonatomic) IBOutlet UITextField *homeServerTextField;
@property (weak, nonatomic) IBOutlet UILabel *homeServerInfoLabel;
@property (weak, nonatomic) IBOutlet UITextField *identityServerTextField;
@property (weak, nonatomic) IBOutlet UILabel *identityServerInfoLabel;
@property (weak, nonatomic) IBOutlet UIButton *submitButton;
@property (weak, nonatomic) IBOutlet UIButton *authSwitchButton;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@property (weak, nonatomic) IBOutlet UILabel *noFlowLabel;
@property (weak, nonatomic) IBOutlet UIButton *retryButton;
@property (weak, nonatomic) IBOutlet UIView *registrationFallbackContentView;
@property (weak, nonatomic) IBOutlet MXCRegistrationWebView *registrationFallbackWebView;
@property (weak, nonatomic) IBOutlet UIButton *cancelRegistrationFallbackButton;
@end
@implementation AuthenticationViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Force contentView in full width
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.contentView
attribute:NSLayoutAttributeLeading
relatedBy:0
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
[self.view addConstraint:leftConstraint];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.contentView
attribute:NSLayoutAttributeTrailing
relatedBy:0
toItem:self.view
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:0];
[self.view addConstraint:rightConstraint];
_authenticationScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_submitButton.enabled = NO;
_authSwitchButton.enabled = YES;
_authInputsPasswordBasedView.delegate = self;
_authInputsEmailCodeBasedView.delegate = self;
supportedFlows = [NSMutableArray array];
_homeServerTextField.text = [[MatrixSDKHandler sharedHandler] homeServerURL];
_identityServerTextField.text = [[MatrixSDKHandler sharedHandler] identityServerURL];
// Set initial auth type
_authType = AuthenticationTypeLogin;
}
- (void)dealloc {
supportedFlows = nil;
if (mxCurrentOperation){
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Update supported authentication flow
self.authType = _authType;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self dismissKeyboard];
// close any opened alert
if (alert) {
[alert dismiss:NO];
alert = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
}
#pragma mark -
+ (BOOL)isImplementedFlowType:(NSString*)flowType forAuthType:(AuthenticationType)authType {
if (authType == AuthenticationTypeLogin) {
if ([flowType isEqualToString:kMXLoginFlowTypePassword]
/*|| [flowType isEqualToString:kMXLoginFlowTypeEmailCode]*/) {
return YES;
}
} else { // AuthenticationTypeRegister
// No registration flow is supported yet
}
return NO;
}
- (void)setAuthType:(AuthenticationType)authType {
if (authType == AuthenticationTypeLogin) {
_createAccountLabel.hidden = YES;
[_submitButton setTitle:@"Login" forState:UIControlStateNormal];
[_submitButton setTitle:@"Login" forState:UIControlStateHighlighted];
[_authSwitchButton setTitle:@"Create account" forState:UIControlStateNormal];
[_authSwitchButton setTitle:@"Create account" forState:UIControlStateHighlighted];
} else {
_createAccountLabel.hidden = NO;
[_submitButton setTitle:@"Sign up" forState:UIControlStateNormal];
[_submitButton setTitle:@"Sign up" forState:UIControlStateHighlighted];
[_authSwitchButton setTitle:@"Back" forState:UIControlStateNormal];
[_authSwitchButton setTitle:@"Back" forState:UIControlStateHighlighted];
}
_authType = authType;
// Update supported authentication flow
[self refreshSupportedAuthFlow];
}
- (void)setSelectedFlow:(MXLoginFlow *)selectedFlow {
// Hide views which depend on auth flow
_submitButton.hidden = YES;
_authInputsPasswordBasedView.hidden = YES;
_authInputsEmailCodeBasedView.hidden = YES;
_noFlowLabel.hidden = YES;
_retryButton.hidden = YES;
currentAuthInputsView = nil;
// Select the right auth inputs view
if ([selectedFlow.type isEqualToString:kMXLoginFlowTypePassword]) {
currentAuthInputsView = _authInputsPasswordBasedView;
} else if ([selectedFlow.type isEqualToString:kMXLoginFlowTypeEmailCode]) {
currentAuthInputsView = _authInputsEmailCodeBasedView;
}
if (currentAuthInputsView) {
_submitButton.hidden = NO;
currentAuthInputsView.hidden = NO;
currentAuthInputsView.authType = _authType;
_authInputContainerViewHeightConstraint.constant = currentAuthInputsView.actualHeight;
} else {
// No input fields are displayed
_authInputContainerViewHeightConstraint.constant = 80;
}
[self.view layoutIfNeeded];
// Refresh content view height
_contentViewHeightConstraint.constant = _authSwitchButton.frame.origin.y + _authSwitchButton.frame.size.height + 15;
_selectedFlow = selectedFlow;
}
- (void)setUserInteractionEnabled:(BOOL)isEnabled {
_submitButton.enabled = (isEnabled && currentAuthInputsView.areAllRequiredFieldsFilled && _homeServerTextField.text.length);
_authSwitchButton.enabled = isEnabled;
_homeServerTextField.enabled = isEnabled;
_identityServerTextField.enabled = isEnabled;
}
- (void)refreshSupportedAuthFlow {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
// Remove reachability observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:AFNetworkingReachabilityDidChangeNotification object:nil];
// Cancel potential request in progress
[mxCurrentOperation cancel];
mxCurrentOperation = nil;
[_activityIndicator startAnimating];
self.selectedFlow = nil;
if (_authType == AuthenticationTypeLogin) {
mxCurrentOperation = [mxHandler.mxRestClient getLoginFlow:^(NSArray *flows) {
[self handleHomeServerFlows:flows];
} failure:^(NSError *error) {
NSLog(@"[AuthenticationVC] Failed to get Login flows: %@", error);
[self onFailureDuringMXOperation:error];
}];
} else {
// mxCurrentOperation = [mxHandler.mxRestClient getRegisterFlow:^(NSArray *flows) {
// [self handleHomeServerFlows:flows];
// } failure:^(NSError *error) {
// NSLog(@"[AuthenticationVC] Failed to get Register flows: %@", error);
// [self onFailureDuringMXOperation:error];
// }];
// Currently no registration flow are supported, we switch directly to the fallback page
[self showRegistrationFallBackView:[mxHandler.mxRestClient registerFallback]];
}
}
- (void)handleHomeServerFlows:(NSArray *)flows {
[_activityIndicator stopAnimating];
[supportedFlows removeAllObjects];
for (MXLoginFlow* flow in flows) {
if ([AuthenticationViewController isImplementedFlowType:flow.type forAuthType:_authType]) {
// Check here all stages
BOOL isSupported = YES;
if (flow.stages.count) {
for (NSString *stage in flow.stages) {
if ([AuthenticationViewController isImplementedFlowType:stage forAuthType:_authType] == NO) {
isSupported = NO;
break;
}
}
}
if (isSupported) {
[supportedFlows addObject:flow];
}
}
}
if (supportedFlows.count) {
// FIXME display supported flows
// Currently we select the first one
self.selectedFlow = [supportedFlows firstObject];
}
if (!_selectedFlow) {
// Notify user that no flow is supported
if (_authType == AuthenticationTypeLogin) {
_noFlowLabel.text = @"Currently we do not support Login flows defined by this Home Server.";
} else {
_noFlowLabel.text = @"Registration is not currently supported.";
}
NSLog(@"[AuthenticationVC] Warning: %@", _noFlowLabel.text);
_noFlowLabel.hidden = NO;
_retryButton.hidden = NO;
}
}
- (void)onFailureDuringMXOperation:(NSError*)error {
mxCurrentOperation = nil;
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == kCFURLErrorCancelled) {
// Ignore this error
return;
}
[_activityIndicator stopAnimating];
// Alert user
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
if (!title)
{
title = @"Error";
}
NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
alert = [[MXCAlert alloc] initWithTitle:title message:msg style:MXCAlertStyleAlert];
alert.cancelButtonIndex = [alert addActionWithTitle:@"Dismiss" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {}];
[alert showInViewController:self];
// Display failure reason
_noFlowLabel.hidden = NO;
_noFlowLabel.text = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
if (!_noFlowLabel.text.length) {
_noFlowLabel.text = @"We failed to retrieve authentication information from this Home Server";
}
_retryButton.hidden = NO;
// Handle specific error code here
if ([error.domain isEqualToString:NSURLErrorDomain]) {
// Check network reachability
if (error.code == NSURLErrorNotConnectedToInternet) {
// Add reachability observer in order to launch a new request when network will be available
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReachabilityStatusChange:) name:AFNetworkingReachabilityDidChangeNotification object:nil];
} else if (error.code == kCFURLErrorTimedOut) {
// Send a new request in 2 sec
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self refreshSupportedAuthFlow];
});
}
}
}
- (void)onReachabilityStatusChange:(NSNotification *)notif {
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
AFNetworkReachabilityStatus status = reachabilityManager.networkReachabilityStatus;
if (status == AFNetworkReachabilityStatusReachableViaWiFi || status == AFNetworkReachabilityStatusReachableViaWWAN) {
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshSupportedAuthFlow];
});
} else if (status == AFNetworkReachabilityStatusNotReachable) {
_noFlowLabel.text = @"Please check your network connectivity";
}
}
- (IBAction)onButtonPressed:(id)sender {
[self dismissKeyboard];
if (sender == _submitButton) {
MatrixSDKHandler *matrix = [MatrixSDKHandler sharedHandler];
if (matrix.mxRestClient) {
// Disable user interaction to prevent multiple requests
[self setUserInteractionEnabled:NO];
[_activityIndicator startAnimating];
if (_authType == AuthenticationTypeLogin) {
if ([_selectedFlow.type isEqualToString:kMXLoginFlowTypePassword]) {
[matrix.mxRestClient loginWithUser:matrix.userLogin andPassword:_authInputsPasswordBasedView.passWordTextField.text
success:^(MXCredentials *credentials){
[_activityIndicator stopAnimating];
// Report credentials
[matrix setUserId:credentials.userId];
[matrix setAccessToken:credentials.accessToken];
// Extract homeServer name from userId
NSArray *components = [credentials.userId componentsSeparatedByString:@":"];
if (components.count == 2) {
[matrix setHomeServer:[components lastObject]];
} else {
NSLog(@"[AuthenticationVC] Warning: the userId is not correctly formatted: %@", credentials.userId);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
failure:^(NSError *error){
[self onFailureDuringAuthRequest:error];
}];
} else {
// FIXME
[self onFailureDuringAuthRequest:[NSError errorWithDomain:nil code:0 userInfo:@{@"error": @"Not supported yet"}]];
}
} else {
// FIXME
[self onFailureDuringAuthRequest:[NSError errorWithDomain:nil code:0 userInfo:@{@"error": @"Not supported yet"}]];
}
}
} else if (sender == _authSwitchButton){
if (_authType == AuthenticationTypeLogin) {
self.authType = AuthenticationTypeRegister;
} else {
self.authType = AuthenticationTypeLogin;
}
} else if (sender == _retryButton) {
[self refreshSupportedAuthFlow];
} else if (sender == _cancelRegistrationFallbackButton) {
// Hide fallback webview
[self hideRegistrationFallbackView];
self.authType = AuthenticationTypeLogin;
}
}
- (void)onFailureDuringAuthRequest:(NSError *)error {
[_activityIndicator stopAnimating];
[self setUserInteractionEnabled:YES];
NSLog(@"[AuthenticationVC] Auth request failed: %@", error);
// translate the error code to a human message
NSString* message = error.localizedDescription;
NSDictionary* dict = error.userInfo;
// detect if it is a Matrix SDK issue
if (dict) {
NSString* localizedError = [dict valueForKey:@"error"];
NSString* errCode = [dict valueForKey:@"errcode"];
if (errCode) {
if ([errCode isEqualToString:@"M_FORBIDDEN"]) {
message = @"Invalid username/password";
} else if (localizedError.length > 0) {
message = localizedError;
} else if ([errCode isEqualToString:@"M_UNKNOWN_TOKEN"]) {
message = @"The access token specified was not recognised";
} else if ([errCode isEqualToString:@"M_BAD_JSON"]) {
message = @"Malformed JSON";
} else if ([errCode isEqualToString:@"M_NOT_JSON"]) {
message = @"Did not contain valid JSON";
} else if ([errCode isEqualToString:@"M_LIMIT_EXCEEDED"]) {
message = @"Too many requests have been sent";
} else if ([errCode isEqualToString:@"M_USER_IN_USE"]) {
message = @"This user name is already used";
} else if ([errCode isEqualToString:@"M_LOGIN_EMAIL_URL_NOT_YET"]) {
message = @"The email link which has not been clicked yet";
} else {
message = errCode;
}
}
}
//Alert user
alert = [[MXCAlert alloc] initWithTitle:@"Login Failed" message:message style:MXCAlertStyleAlert];
[alert addActionWithTitle:@"Dismiss" style:MXCAlertActionStyleCancel handler:^(MXCAlert *alert) {}];
[alert showInViewController:self];
}
#pragma mark - Keyboard handling
- (void)onKeyboardWillShow:(NSNotification *)notif {
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
CGRect endRect = rectVal.CGRectValue;
UIEdgeInsets insets = self.authenticationScrollView.contentInset;
// Handle portrait/landscape mode
insets.bottom = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
self.authenticationScrollView.contentInset = insets;
}
- (void)onKeyboardWillHide:(NSNotification *)notif {
UIEdgeInsets insets = self.authenticationScrollView.contentInset;
insets.bottom = 0;
self.authenticationScrollView.contentInset = insets;
}
- (void)dismissKeyboard {
// Hide the keyboard
[currentAuthInputsView dismissKeyboard];
[_homeServerTextField resignFirstResponder];
[_identityServerTextField resignFirstResponder];
}
#pragma mark - UITextField delegate
- (void)onTextFieldChange:(NSNotification *)notif {
NSString *homeServerURL = _homeServerTextField.text;
if (currentAuthInputsView.areAllRequiredFieldsFilled && homeServerURL.length) {
_submitButton.enabled = YES;
} else {
_submitButton.enabled = NO;
}
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
if (textField == _homeServerTextField) {
if (![[mxHandler homeServerURL] isEqualToString:textField.text]) {
[mxHandler setHomeServerURL:textField.text];
if (!textField.text.length) {
// Force refresh with default value
textField.text = [mxHandler homeServerURL];
}
// Refresh UI
[self refreshSupportedAuthFlow];
}
}
else if (textField == _identityServerTextField) {
[mxHandler setIdentityServerURL:textField.text];
if (!textField.text.length) {
// Force refresh with default value
textField.text = [mxHandler identityServerURL];
}
}
}
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
if (textField.returnKeyType == UIReturnKeyDone) {
// "Done" key has been pressed
[textField resignFirstResponder];
}
return YES;
}
#pragma mark - AuthInputsViewDelegate delegate
- (void)authInputsDoneKeyHasBeenPressed:(AuthInputsView *)authInputsView {
if (_submitButton.isEnabled) {
// Launch authentication now
[self onButtonPressed:_submitButton];
}
}
#pragma mark - Registration Fallback
- (void)showRegistrationFallBackView:(NSString*)fallbackPage {
_authenticationScrollView.hidden = YES;
_registrationFallbackContentView.hidden = NO;
[_registrationFallbackWebView openFallbackPage:fallbackPage success:^(MXCredentials *credentials) {
// Report credentials
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[mxHandler setUserId:credentials.userId];
[mxHandler setAccessToken:credentials.accessToken];
// Extract homeServer name from userId
NSArray *components = [credentials.userId componentsSeparatedByString:@":"];
if (components.count == 2) {
[mxHandler setHomeServer:[components lastObject]];
} else {
NSLog(@"[AuthenticationVC] Warning: the userId is not correctly formatted: %@", credentials.userId);
}
[self dismissViewControllerAnimated:YES completion:nil];
}];
}
- (void)hideRegistrationFallbackView {
[_registrationFallbackWebView stopLoading];
_authenticationScrollView.hidden = NO;
_registrationFallbackContentView.hidden = YES;
}
@end
@@ -0,0 +1,25 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
#import "MXCContact.h"
@interface ContactDetailsViewController : MXCTableViewController
@property (strong, nonatomic) MXCContact* contact;
@end
@@ -0,0 +1,126 @@
/*
Copyright 2014 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 "ContactDetailsViewController.h"
#import "ContactDetailsTableCell.h"
#import "MatrixSDKHandler.h"
@interface ContactDetailsViewController () {
NSArray* matrixIDs;
}
@property (weak, nonatomic) IBOutlet UIButton *memberThumbnailButton;
@property (weak, nonatomic) IBOutlet UITextView *roomMemberMID;
@end
@implementation ContactDetailsViewController
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.memberThumbnailButton = nil;
self.roomMemberMID = nil;
matrixIDs = nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
// remove the line separator color
self.tableView.separatorColor = [UIColor clearColor];
self.tableView.rowHeight = 44;
self.tableView.allowsSelection = NO;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.roomMemberMID.text = _contact.displayName;
// set the thumbnail info
[self.memberThumbnailButton.imageView setContentMode: UIViewContentModeScaleAspectFill];
[self.memberThumbnailButton.imageView setClipsToBounds:YES];
if (_contact.thumbnail) {
self.memberThumbnailButton.imageView.image = _contact.thumbnail;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThumbnailUpdate:) name:kMXCContactThumbnailUpdateNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
matrixIDs = _contact.matrixIdentifiers;
return matrixIDs.count;
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
ContactDetailsTableCell* contactDetailsTableCell = (ContactDetailsTableCell*)[aTableView dequeueReusableCellWithIdentifier:@"ContactDetailsTableCell" forIndexPath:indexPath];
if (row < matrixIDs.count) {
contactDetailsTableCell.matrixUserIDLabel.text = [matrixIDs objectAtIndex:row];
} else {
// should never happen
contactDetailsTableCell.matrixUserIDLabel.text = @"";
}
[contactDetailsTableCell.startChatButton addTarget:self action:@selector(startChat:) forControlEvents:UIControlEventTouchUpInside];
return contactDetailsTableCell;
}
- (void)startChat:(UIButton*)sender {
UIView* view = sender;
// search the parentce cell
while (view && ![view isKindOfClass:[ContactDetailsTableCell class]]) {
view = view.superview;
}
if ([view isKindOfClass:[ContactDetailsTableCell class]]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[MatrixSDKHandler sharedHandler] startPrivateOneToOneRoomWithUserId:((ContactDetailsTableCell*)view).matrixUserIDLabel.text];
});
}
}
- (void)onThumbnailUpdate:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* matrixID = notif.object;
if ([matrixID isEqualToString:self.contact.contactID]) {
if (_contact.thumbnail) {
self.memberThumbnailButton.imageView.image = _contact.thumbnail;
}
}
}
}
@end
@@ -0,0 +1,30 @@
/*
Copyright 2014 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 "MXCViewController.h"
// SMS
#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMessageComposeViewController.h>
#import "SectionedContacts.h"
@interface ContactsViewController : MXCViewController <UITableViewDataSource, UITableViewDelegate, UINavigationControllerDelegate, MFMessageComposeViewControllerDelegate, UISearchBarDelegate, UIGestureRecognizerDelegate> {
NSArray* collationTitles;
}
@end
@@ -0,0 +1,765 @@
/*
Copyright 2014 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"
// SDK api
#import "MatrixSDKHandler.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"
// alert
#import "MXCAlert.h"
// settings
#import "AppSettings.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) MXCAlert *startChatMenu;
@property (strong, nonatomic) MXCAlert *allowContactSyncAlert;
@property (weak, nonatomic) IBOutlet UITableView* tableView;
@property (weak, nonatomic) IBOutlet UISegmentedControl* contactsControls;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@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];
// event listener
[[MatrixSDKHandler sharedHandler] addObserver:self forKeyPath:@"status" options:0 context:nil];
// 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];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// required to reduce the tableview height while searching
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Leave potential search session
if (contactsSearchBar) {
[self searchBarCancelButtonClicked:contactsSearchBar];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
}
- (void)startActivityIndicator {
[_activityIndicator.layer setCornerRadius:5];
_activityIndicator.hidden = NO;
[_activityIndicator startAnimating];
}
- (void)stopActivityIndicator {
[_activityIndicator stopAnimating];
_activityIndicator.hidden = YES;
}
- (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];
}
#pragma mark - Keyboard handling
- (void)onKeyboardWillShow:(NSNotification *)notif {
// get the keyboard size
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
CGRect endRect = rectVal.CGRectValue;
// IOS 8 triggers some unexpected keyboard events
if ((endRect.size.height == 0) || (endRect.size.width == 0)) {
return;
}
CGFloat keyboardHeight = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
// the tableview bottom inset must also be updated
UIEdgeInsets insets = self.tableView.contentInset;
insets.bottom = keyboardHeight;
// get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// the duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
// reduce the tableview height
self.tableView.contentInset = insets;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
}
- (void)onKeyboardWillHide:(NSNotification *)notif {
// get the keyboard size
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
CGRect endRect = rectVal.CGRectValue;
rectVal = notif.userInfo[UIKeyboardFrameBeginUserInfoKey];
CGRect beginRect = rectVal.CGRectValue;
UIEdgeInsets insets = self.tableView.contentInset;
insets.bottom = 0;
// do not animate if the both rect are the same
// but ensure that the fields are properly resetted
// e.g. when the user swipes to hide the keyboard
// this method is called with invalid rects
// animationDuration is ignored because of the animation curve
// use it to be sure that it will be broken with any new IOS update
if (CGRectEqualToRect(endRect, beginRect)) {
self.tableView.contentInset = insets;
[self.view layoutIfNeeded];
} else {
// get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// the duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// animate the keyboard closing
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
self.tableView.contentInset = insets;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
}
}
#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 in matrix handler
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
if (!mxHandler.mxSession) {
[self startActivityIndicator];
sectionedMatrixContacts = nil;
} else {
[self stopActivityIndicator];
NSArray* usersIDs = [mxHandler 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 = [mxHandler.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 {
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;
}
- (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) {
MatrixSDKHandler* mxHandler = [MatrixSDKHandler sharedHandler];
// display only if the mxSession is available in matrix SDK handler
if (mxHandler.mxSession) {
// only 1 matrix ID
if (matrixIDs.count == 1) {
NSString* matrixID = [matrixIDs objectAtIndex:0];
self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with %@", matrixID] message:nil style:MXCAlertStyleAlert];
[self.startChatMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.startChatMenu = nil;
}];
[self.startChatMenu addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.startChatMenu = nil;
[mxHandler startPrivateOneToOneRoomWithUserId:matrixID];
}];
} else {
self.startChatMenu = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Chat with "] message:nil style:MXCAlertStyleActionSheet];
for(NSString* matrixID in matrixIDs) {
[self.startChatMenu addActionWithTitle:matrixID style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.startChatMenu = nil;
[mxHandler startPrivateOneToOneRoomWithUserId:matrixID];
}];
}
[self.startChatMenu addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *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 = [[MXCAlert alloc] initWithTitle:[NSString stringWithFormat:@"Invite this user to use matrix with"] message:nil style:MXCAlertStyleActionSheet];
// check if the target can send SMSes
if ([MFMessageComposeViewController canSendText]) {
// list phonenumbers
for(MXCPhoneNumber* phonenumber in contact.phoneNumbers) {
[self.startChatMenu addActionWithTitle:phonenumber.textNumber style:MXCAlertActionStyleDefault handler:^(MXCAlert *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:MXCAlertActionStyleDefault handler:^(MXCAlert *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:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.startChatMenu = nil;
}];
UIView *sourceView = [tableView cellForRowAtIndexPath:indexPath];
self.startChatMenu.sourceView = sourceView ? sourceView : tableView;
[self.startChatMenu showInViewController:self];
}
}
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"status" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
if (displayMatrixUsers) {
if (contactsSearchBar) {
[self updateSectionedMatrixContacts];
latestSearchedPattern = nil;
[self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text];
} else {
[self.tableView reloadData];
}
}
});
}
}
#pragma mark - Actions
- (void)onContactsRefresh:(NSNotification *)notif {
localContacts = nil;
sectionedLocalContacts = nil;
// there is an user id
if ([[MatrixSDKHandler sharedHandler] 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) {
AppSettings* appSettings = [AppSettings sharedSettings];
if (!appSettings.syncLocalContacts) {
__weak typeof(self) weakSelf = self;
self.allowContactSyncAlert = [[MXCAlert alloc] initWithTitle:@"Allow local contacts synchronization ?" message:nil style:MXCAlertStyleAlert];
[self.allowContactSyncAlert addActionWithTitle:@"No" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.allowContactSyncAlert = nil;
}];
[self.allowContactSyncAlert addActionWithTitle:@"Yes" style:MXCAlertActionStyleDefault handler:^(MXCAlert *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.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.tableHeaderView = contactsSearchBar;
[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 = nil;
filteredContacts = nil;
sectionedFilteredContacts = nil;
latestSearchedPattern = nil;
self.tableView.tableHeaderView = nil;
[self.tableView reloadData];
[self scrollToTop];
}
}
@end
@@ -0,0 +1,22 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
@interface HomeViewController : MXCTableViewController <UITextFieldDelegate, UISearchBarDelegate>
@end
@@ -0,0 +1,636 @@
/*
Copyright 2014 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 "HomeViewController.h"
#import "MatrixSDKHandler.h"
#import "AppDelegate.h"
#import "PublicRoomTableCell.h"
@interface HomeViewController () {
NSArray *publicRooms;
// List of public room names to highlight in displayed list
NSArray* highlightedPublicRooms;
// Search in public room
UISearchBar *publicRoomsSearchBar;
NSMutableArray *filteredPublicRooms;
BOOL searchBarShouldEndEditing;
UIView *savedTableHeaderView;
NSString *homeServerSuffix;
}
@property (weak, nonatomic) IBOutlet UITableView *publicRoomsTable;
@property (weak, nonatomic) IBOutlet UILabel *roomCreationSectionLabel;
@property (weak, nonatomic) IBOutlet UIView *roomCreationSectionView;
@property (weak, nonatomic) IBOutlet UILabel *roomNameLabel;
@property (weak, nonatomic) IBOutlet UILabel *roomAliasLabel;
@property (weak, nonatomic) IBOutlet UILabel *participantsLabel;
@property (weak, nonatomic) IBOutlet UITextField *roomNameTextField;
@property (weak, nonatomic) IBOutlet UITextField *roomAliasTextField;
@property (weak, nonatomic) IBOutlet UITextField *participantsTextField;
@property (weak, nonatomic) IBOutlet UISegmentedControl *roomVisibilityControl;
@property (weak, nonatomic) IBOutlet UIButton *createRoomBtn;
@property (weak, nonatomic) IBOutlet UILabel *joinRoomSectionLabel;
@property (weak, nonatomic) IBOutlet UITextField *joinRoomAliasTextField;
@property (weak, nonatomic) IBOutlet UIButton *joinRoomBtn;
- (IBAction)onButtonPressed:(id)sender;
@end
@implementation HomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_roomCreationSectionLabel.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
_createRoomBtn.enabled = NO;
_joinRoomSectionLabel.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
_joinRoomBtn.enabled = NO;
// Init
publicRooms = nil;
highlightedPublicRooms = @[@"#matrix:matrix.org", @"#matrix-dev:matrix.org", @"#matrix-fr:matrix.org"]; // Add here a room name to highlight its display in public room list
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)dealloc{
publicRooms = nil;
highlightedPublicRooms = nil;
publicRoomsSearchBar = nil;
filteredPublicRooms = nil;
savedTableHeaderView = nil;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Ensure to display room creation section
[self.tableView scrollRectToVisible:_roomCreationSectionLabel.frame animated:NO];
if ([MatrixSDKHandler sharedHandler].status != MatrixSDKHandlerStatusLoggedOut) {
homeServerSuffix = [NSString stringWithFormat:@":%@",[MatrixSDKHandler sharedHandler].homeServer];
// Update alias placeholder
_roomAliasTextField.placeholder = [NSString stringWithFormat:@"(e.g. #foo%@)", homeServerSuffix];
// Refresh listed public rooms
[self refreshPublicRooms];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Leave potential search session
if (publicRoomsSearchBar) {
[self searchBarCancelButtonClicked:publicRoomsSearchBar];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil];
}
#pragma mark - Internals
- (void)refreshPublicRooms {
// Retrieve public rooms
[[MatrixSDKHandler sharedHandler].mxRestClient publicRooms:^(NSArray *rooms){
publicRooms = [rooms sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
MXPublicRoom *firstRoom = (MXPublicRoom*)a;
MXPublicRoom *secondRoom = (MXPublicRoom*)b;
// Compare member count
if (firstRoom.numJoinedMembers < secondRoom.numJoinedMembers) {
return NSOrderedDescending;
} else if (firstRoom.numJoinedMembers > secondRoom.numJoinedMembers) {
return NSOrderedAscending;
} else {
// Alphabetic order
return [firstRoom.displayname compare:secondRoom.displayname options:NSCaseInsensitiveSearch];
}
}];
[_publicRoomsTable reloadData];
}
failure:^(NSError *error){
NSLog(@"[HomeVC] Failed to get public rooms: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
- (void)search:(id)sender {
if (!publicRoomsSearchBar) {
// Check whether there are data in which search
if (publicRooms.count) {
// Create search bar
publicRoomsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
publicRoomsSearchBar.showsCancelButton = YES;
publicRoomsSearchBar.returnKeyType = UIReturnKeyDone;
publicRoomsSearchBar.delegate = self;
[publicRoomsSearchBar becomeFirstResponder];
publicRoomsSearchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
// Hide table header during search session
savedTableHeaderView = self.tableView.tableHeaderView;
self.tableView.tableHeaderView = nil;
// Reload table in order to display search bar as section header
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
[self.tableView reloadData];
}
} else {
[self searchBarCancelButtonClicked: publicRoomsSearchBar];
}
}
- (void)dismissKeyboard {
// Hide the keyboard
[_roomNameTextField resignFirstResponder];
[_roomAliasTextField resignFirstResponder];
[_participantsTextField resignFirstResponder];
[_joinRoomAliasTextField resignFirstResponder];
}
- (NSString*)alias {
// Extract alias name from alias text field
NSString *alias = _roomAliasTextField.text;
if (alias.length > 1) {
// Remove '#' character
alias = [alias substringFromIndex:1];
// Remove homeserver
NSRange range = [alias rangeOfString:homeServerSuffix];
if (range.location == NSNotFound) {
NSLog(@"[HomeVC] Wrong room alias has been set (%@)", _roomAliasTextField.text);
alias = nil;
} else {
alias = [alias stringByReplacingCharactersInRange:range withString:@""];
}
}
if (! alias.length) {
alias = nil;
}
return alias;
}
- (NSArray*)participantsList {
NSMutableArray *participants = [NSMutableArray array];
if (_participantsTextField.text.length) {
NSArray *components = [_participantsTextField.text componentsSeparatedByString:@";"];
for (NSString *component in components) {
// Remove white space from both ends
NSString *user = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (user.length > 1 && [user hasPrefix:@"@"]) {
[participants addObject:user];
}
}
}
if (participants.count == 0) {
participants = nil;
}
return participants;
}
#pragma mark - UITextField delegate
- (void)onTextFieldChange:(NSNotification *)notif {
// Update Create Room button
NSString *roomName = _roomNameTextField.text;
NSString *roomAlias = _roomAliasTextField.text;
NSString *participants = _participantsTextField.text;
if (roomName.length || roomAlias.length || participants.length) {
_createRoomBtn.enabled = YES;
} else {
_createRoomBtn.enabled = NO;
}
// Update Join Room button
_joinRoomBtn.enabled = (_joinRoomAliasTextField.text.length != 0);
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
if (textField == _participantsTextField) {
if (textField.text.length == 0) {
textField.text = @"@";
}
} else if (textField == _roomAliasTextField || textField == _joinRoomAliasTextField) {
if (textField.text.length == 0) {
textField.text = @"#";
}
}
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField == _roomAliasTextField) {
// Check whether homeserver suffix should be added
NSRange range = [textField.text rangeOfString:@":"];
if (range.location == NSNotFound) {
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
}
// Check whether the alias is valid
if (!self.alias) {
// reset text field
textField.text = nil;
[self onTextFieldChange:nil];
}
} else if (textField == _participantsTextField) {
NSArray *participants = self.participantsList;
textField.text = [participants componentsJoinedByString:@"; "];
} else if (textField == _joinRoomAliasTextField) {
if (textField.text.length > 1) {
// Add homeserver suffix if none
NSRange range = [textField.text rangeOfString:@":"];
if (range.location == NSNotFound) {
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
}
} else {
// reset text field
textField.text = nil;
[self onTextFieldChange:nil];
}
}
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
// Auto complete participant IDs
if (textField == _participantsTextField) {
// Add @ if none
if (!textField.text.length || textField.text.length == range.length) {
if ([string hasPrefix:@"@"] == NO) {
textField.text = [NSString stringWithFormat:@"@%@",string];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
} else if (range.location == textField.text.length) {
if ([string isEqualToString:@";"]) {
// Add '@' character
textField.text = [textField.text stringByAppendingString:@"; @"];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
}
} else if (textField == _roomAliasTextField) {
// Add # if none
if (!textField.text.length || textField.text.length == range.length) {
if ([string hasPrefix:@"#"] == NO) {
if ([string isEqualToString:@":"]) {
textField.text = [NSString stringWithFormat:@"#%@",homeServerSuffix];
} else {
textField.text = [NSString stringWithFormat:@"#%@",string];
}
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
} else {
// Add homeserver automatically when user adds ':' at the end
if (range.location == textField.text.length && [string isEqualToString:@":"]) {
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
}
} else if (textField == _joinRoomAliasTextField) {
// Add # if none
if (!textField.text.length || textField.text.length == range.length) {
if ([string hasPrefix:@"#"] == NO) {
textField.text = [NSString stringWithFormat:@"#%@",string];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
}
}
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
// "Done" key has been pressed
[textField resignFirstResponder];
return YES;
}
#pragma mark - Actions
- (IBAction)onButtonPressed:(id)sender {
[self dismissKeyboard];
if (sender == _createRoomBtn) {
// Disable button to prevent multiple request
_createRoomBtn.enabled = NO;
NSString *roomName = _roomNameTextField.text;
if (! roomName.length) {
roomName = nil;
}
// Create new room
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[mxHandler.mxRestClient createRoom:roomName
visibility:(_roomVisibilityControl.selectedSegmentIndex == 0) ? kMXRoomVisibilityPublic : kMXRoomVisibilityPrivate
roomAlias:self.alias
topic:nil
success:^(MXCreateRoomResponse *response) {
// Check whether some users must be invited
NSArray *invitedUsers = self.participantsList;
for (NSString *userId in invitedUsers) {
[mxHandler.mxRestClient inviteUser:userId toRoom:response.roomId success:^{
NSLog(@"[HomeVC] %@ has been invited (roomId: %@)", userId, response.roomId);
} failure:^(NSError *error) {
NSLog(@"[HomeVC] %@ invitation failed (roomId: %@): %@", userId, response.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
// Reset text fields
_roomNameTextField.text = nil;
_roomAliasTextField.text = nil;
_participantsTextField.text = nil;
// Open created room
[[AppDelegate theDelegate].masterTabBarController showRoom:response.roomId];
} failure:^(NSError *error) {
_createRoomBtn.enabled = YES;
NSLog(@"[HomeVC] Create room (%@ %@ (%@)) failed: %@", _roomNameTextField.text, self.alias, (_roomVisibilityControl.selectedSegmentIndex == 0) ? @"Public":@"Private", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if (sender == _joinRoomBtn) {
// Disable button to prevent multiple request
_joinRoomBtn.enabled = NO;
NSString *roomAlias = _joinRoomAliasTextField.text;
// Remove white space from both ends
roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// Check
if (roomAlias.length) {
[[MatrixSDKHandler sharedHandler].mxSession joinRoom:roomAlias success:^(MXRoom *room) {
// Reset text fields
_joinRoomAliasTextField.text = nil;
// Show the room
[[AppDelegate theDelegate].masterTabBarController showRoom:room.state.roomId];
} failure:^(NSError *error) {
_joinRoomBtn.enabled = YES;
NSLog(@"[HomeVC] Failed to join room alias (%@): %@", roomAlias, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else {
// Reset text fields
_joinRoomAliasTextField.text = nil;
}
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (filteredPublicRooms) {
return filteredPublicRooms.count;
}
return publicRooms.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (publicRoomsSearchBar) {
return (publicRoomsSearchBar.frame.size.height + 40);
}
return 40;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *sectionHeader = [[UIView alloc] initWithFrame:[tableView rectForHeaderInSection:section]];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
UILabel *sectionLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sectionHeader.frame.size.width, 40)];
sectionLabel.font = [UIFont boldSystemFontOfSize:16];
sectionLabel.backgroundColor = [UIColor clearColor];
[sectionHeader addSubview:sectionLabel];
if (publicRooms) {
NSString *homeserver = [MatrixSDKHandler sharedHandler].homeServerURL;
if (homeserver.length) {
sectionLabel.text = [NSString stringWithFormat:@" Public Rooms (at %@):", homeserver];
} else {
sectionLabel.text = @" Public Rooms:";
}
UIButton *searchButton = [UIButton buttonWithType:UIButtonTypeCustom];
[searchButton setImage:[UIImage imageNamed:@"icon_search"] forState:UIControlStateNormal];
[searchButton setImage:[UIImage imageNamed:@"icon_search"] forState:UIControlStateHighlighted];
[searchButton addTarget:self action:@selector(search:) forControlEvents:UIControlEventTouchUpInside];
searchButton.frame = CGRectMake(sectionLabel.frame.size.width - 45, 0, 40, 40);
searchButton.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
[sectionHeader addSubview:searchButton];
sectionHeader.userInteractionEnabled = YES;
if (publicRoomsSearchBar) {
CGRect frame = publicRoomsSearchBar.frame;
frame.origin.y = 40;
publicRoomsSearchBar.frame = frame;
[sectionHeader addSubview:publicRoomsSearchBar];
}
} else {
sectionLabel.text = @" No Public Rooms";
}
return sectionHeader;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Cell is larger for public room with topic
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
if (publicRoom.topic) {
return 60;
}
return 44;
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PublicRoomTableCell *cell;
PublicRoomWithTopicTableCell *cellWithTopic = nil;
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
// Check whether this public room has topic
if (publicRoom.topic) {
cellWithTopic = [_publicRoomsTable dequeueReusableCellWithIdentifier:@"PublicRoomWithTopicCell" forIndexPath:indexPath];
cellWithTopic.roomTopic.text = publicRoom.topic;
cell = cellWithTopic;
} else {
cell = [_publicRoomsTable dequeueReusableCellWithIdentifier:@"PublicRoomCell" forIndexPath:indexPath];
}
// Set room display name
cell.roomDisplayName.text = [publicRoom displayname];
// Set member count
if (publicRoom.numJoinedMembers > 1) {
cell.memberCount.text = [NSString stringWithFormat:@"%lu users", (unsigned long)publicRoom.numJoinedMembers];
} else if (publicRoom.numJoinedMembers == 1) {
cell.memberCount.text = @"1 user";
} else {
cell.memberCount.text = nil;
}
// Highlight?
if (cell.roomDisplayName.text && [highlightedPublicRooms indexOfObject:cell.roomDisplayName.text] != NSNotFound) {
cell.roomDisplayName.font = [UIFont boldSystemFontOfSize:20];
if (cellWithTopic) {
cellWithTopic.roomTopic.font = [UIFont boldSystemFontOfSize:17];
}
cell.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:0.9 alpha:1.0];
} else {
cell.roomDisplayName.font = [UIFont systemFontOfSize:19];
if (cellWithTopic) {
cellWithTopic.roomTopic.font = [UIFont systemFontOfSize:16];
}
cell.backgroundColor = [UIColor clearColor];
}
return cell;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MXPublicRoom *publicRoom;
if (filteredPublicRooms) {
publicRoom = [filteredPublicRooms objectAtIndex:indexPath.row];
} else {
publicRoom = [publicRooms objectAtIndex:indexPath.row];
}
// Check whether the user has already joined the selected public room
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
if ([mxHandler.mxSession roomWithRoomId:publicRoom.roomId]) {
// Open selected room
[[AppDelegate theDelegate].masterTabBarController showRoom:publicRoom.roomId];
} else {
// Join the selected room
UIActivityIndicatorView *loadingWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
if (selectedCell) {
CGPoint center = CGPointMake(selectedCell.frame.size.width / 2, selectedCell.frame.size.height / 2);
loadingWheel.center = center;
[selectedCell addSubview:loadingWheel];
}
[loadingWheel startAnimating];
[mxHandler.mxSession joinRoom:publicRoom.roomId success:^(MXRoom *room) {
// Show joined room
[loadingWheel stopAnimating];
[loadingWheel removeFromSuperview];
[[AppDelegate theDelegate].masterTabBarController showRoom:publicRoom.roomId];
} failure:^(NSError *error) {
NSLog(@"[HomeVC] Failed to join public room (%@): %@", publicRoom.displayname, error);
//Alert user
[loadingWheel stopAnimating];
[loadingWheel removeFromSuperview];
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - UISearchBarDelegate
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
searchBarShouldEndEditing = NO;
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar {
return searchBarShouldEndEditing;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
// Update filtered list
if (searchText.length) {
if (filteredPublicRooms) {
[filteredPublicRooms removeAllObjects];
} else {
filteredPublicRooms = [NSMutableArray arrayWithCapacity:publicRooms.count];
}
for (MXPublicRoom *publicRoom in publicRooms) {
if ([[publicRoom displayname] rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[filteredPublicRooms addObject:publicRoom];
}
}
} else {
filteredPublicRooms = nil;
}
// Refresh display
[self.tableView reloadData];
if (filteredPublicRooms.count) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// "Done" key has been pressed
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
// Leave search
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
publicRoomsSearchBar = nil;
filteredPublicRooms = nil;
// Restore table header and refresh table display
self.tableView.tableHeaderView = savedTableHeaderView;
[self.tableView reloadData];
}
@end
@@ -0,0 +1,30 @@
/*
Copyright 2014 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 <UIKit/UIKit.h>
#import "AFNetworkReachabilityManager.h"
/**
* MXCTableViewController extends UITableViewController for the following points:
* - support rage shake mechanism
* - update navigation bar tintColor according to network reachability
*/
@interface MXCTableViewController : UITableViewController
@end
@@ -0,0 +1,110 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
#import "RageShakableUIResponder.h"
@interface MXCTableViewController () {
id mxcTableViewControllerReachabilityObserver;
}
@end
@implementation MXCTableViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[RageShakableUIResponder cancel:self];
if (self.navigationController) {
// The navigation bar tintColor depends on reachability status - Register reachability observer
__weak typeof(self) weakSelf = self;
mxcTableViewControllerReachabilityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingReachabilityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
[weakSelf onReachabilityStatusChange];
}];
// Force update
[self onReachabilityStatusChange];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:mxcTableViewControllerReachabilityObserver];
[RageShakableUIResponder cancel:self];
}
#pragma mark - Reachability monitoring
- (void)onReachabilityStatusChange {
// Retrieve the current reachability status
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
AFNetworkReachabilityStatus status = reachabilityManager.networkReachabilityStatus;
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = nil;
if (self.splitViewController) {
mainNavigationController = self.navigationController;
UIViewController *parentViewController = self.parentViewController;
while (parentViewController) {
if (parentViewController.navigationController) {
mainNavigationController = parentViewController.navigationController;
parentViewController = parentViewController.parentViewController;
} else {
break;
}
}
}
// Update navigationBar tintColor
if (status == AFNetworkReachabilityStatusNotReachable) {
self.navigationController.navigationBar.barTintColor = [UIColor redColor];
if (mainNavigationController) {
mainNavigationController.navigationBar.barTintColor = [UIColor redColor];
}
} else if (status == AFNetworkReachabilityStatusReachableViaWiFi || status == AFNetworkReachabilityStatusReachableViaWWAN) {
self.navigationController.navigationBar.barTintColor = nil;
if (mainNavigationController) {
mainNavigationController.navigationBar.barTintColor = nil;
}
}
}
#pragma mark - Rage shake handling
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake) {
[RageShakableUIResponder startShaking:self];
}
}
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event {
[self motionEnded:motion withEvent:event];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake) {
[RageShakableUIResponder stopShaking:self];
}
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
@end
@@ -0,0 +1,30 @@
/*
Copyright 2014 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 <UIKit/UIKit.h>
#import "AFNetworkReachabilityManager.h"
/**
* MXCViewController extends UIViewController for the following points:
* - support rage shake mechanism
* - update navigation bar tintColor according to network reachability
*/
@interface MXCViewController : UIViewController
@end
@@ -0,0 +1,110 @@
/*
Copyright 2014 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 "MXCViewController.h"
#import "RageShakableUIResponder.h"
@interface MXCViewController () {
id mxcViewControllerReachabilityObserver;
}
@end
@implementation MXCViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[RageShakableUIResponder cancel:self];
if (self.navigationController) {
// The navigation bar tintColor depends on reachability status - Register reachability observer
__weak typeof(self) weakSelf = self;
mxcViewControllerReachabilityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingReachabilityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
[weakSelf onReachabilityStatusChange];
}];
// Force update
[self onReachabilityStatusChange];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:mxcViewControllerReachabilityObserver];
[RageShakableUIResponder cancel:self];
}
#pragma mark - Reachability monitoring
- (void)onReachabilityStatusChange {
// Retrieve the current reachability status
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
AFNetworkReachabilityStatus status = reachabilityManager.networkReachabilityStatus;
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = nil;
if (self.splitViewController) {
mainNavigationController = self.navigationController;
UIViewController *parentViewController = self.parentViewController;
while (parentViewController) {
if (parentViewController.navigationController) {
mainNavigationController = parentViewController.navigationController;
parentViewController = parentViewController.parentViewController;
} else {
break;
}
}
}
// Update navigationBar tintColor
if (status == AFNetworkReachabilityStatusNotReachable) {
self.navigationController.navigationBar.barTintColor = [UIColor redColor];
if (mainNavigationController) {
mainNavigationController.navigationBar.barTintColor = [UIColor redColor];
}
} else if (status == AFNetworkReachabilityStatusReachableViaWiFi || status == AFNetworkReachabilityStatusReachableViaWWAN) {
self.navigationController.navigationBar.barTintColor = nil;
if (mainNavigationController) {
mainNavigationController.navigationBar.barTintColor = nil;
}
}
}
#pragma mark - Rage shake handling
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake) {
[RageShakableUIResponder startShaking:self];
}
}
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event {
[self motionEnded:motion withEvent:event];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake) {
[RageShakableUIResponder stopShaking:self];
}
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
@end
@@ -0,0 +1,39 @@
/*
Copyright 2014 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 <UIKit/UIKit.h>
#define TABBAR_HOME_INDEX 0
#define TABBAR_RECENTS_INDEX 1
#define TABBAR_SETTINGS_INDEX 2
#define TABBAR_COUNT 3
@interface MasterTabBarController : UITabBarController
- (void)showAuthenticationScreen;
- (void)showRoomCreationForm;
- (void)showRoom:(NSString*)roomId;
- (void)popRoomViewControllerAnimated:(BOOL)animated;
- (BOOL)isPresentingMediaPicker;
- (void)presentMediaPicker:(UIImagePickerController*)mediaPicker;
- (void)dismissMediaPicker;
@property (strong, nonatomic) NSString *visibleRoomId; // nil if no room is presently visible
@end
@@ -0,0 +1,150 @@
/*
Copyright 2014 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 "MasterTabBarController.h"
#import "MatrixSDKHandler.h"
#import "RecentsViewController.h"
@interface MasterTabBarController () {
UINavigationController *recentsNavigationController;
RecentsViewController *recentsViewController;
UIImagePickerController *mediaPicker;
}
@end
@implementation MasterTabBarController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// To simplify navigation into the app, we retrieve here the navigation controller and the view controller related
// to the recents list in Recents Tab.
// Note: UISplitViewController is not supported on iPhone for iOS < 8.0
UIViewController* recents = [self.viewControllers objectAtIndex:TABBAR_RECENTS_INDEX];
recentsNavigationController = nil;
if ([recents isKindOfClass:[UISplitViewController class]]) {
UISplitViewController *splitViewController = (UISplitViewController *)recents;
recentsNavigationController = [splitViewController.viewControllers objectAtIndex:0];
} else if ([recents isKindOfClass:[UINavigationController class]]) {
recentsNavigationController = (UINavigationController*)recents;
}
if (recentsNavigationController) {
for (UIViewController *viewController in recentsNavigationController.viewControllers) {
if ([viewController isKindOfClass:[RecentsViewController class]]) {
recentsViewController = (RecentsViewController*)viewController;
}
}
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([MatrixSDKHandler sharedHandler].status == MatrixSDKHandlerStatusLoggedOut) {
[self showAuthenticationScreen];
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)dealloc {
recentsNavigationController = nil;
recentsViewController = nil;
[self dismissMediaPicker];
}
#pragma mark -
- (void)restoreInitialDisplay {
// Dismiss potential media picker
if (mediaPicker) {
if (mediaPicker.delegate && [mediaPicker.delegate respondsToSelector:@selector(imagePickerControllerDidCancel:)]) {
[mediaPicker.delegate imagePickerControllerDidCancel:mediaPicker];
} else {
[self dismissMediaPicker];
}
}
[self popRoomViewControllerAnimated:NO];
}
#pragma mark -
- (void)showAuthenticationScreen {
[self restoreInitialDisplay];
[self performSegueWithIdentifier:@"showAuth" sender:self];
}
- (void)showRoomCreationForm {
// Switch in Home Tab
[self setSelectedIndex:TABBAR_HOME_INDEX];
}
- (void)showRoom:(NSString*)roomId {
[self restoreInitialDisplay];
// Switch on Recents Tab
[self setSelectedIndex:TABBAR_RECENTS_INDEX];
// Select room to display its details (dispatch this action in order to let TabBarController end its refresh)
dispatch_async(dispatch_get_main_queue(), ^{
recentsViewController.preSelectedRoomId = roomId;
});
}
- (void)popRoomViewControllerAnimated:(BOOL)animated {
// Force back to recents list if room details is displayed in Recents Tab
if (recentsViewController) {
[recentsNavigationController popToViewController:recentsViewController animated:animated];
// Release the current selected room
recentsViewController.preSelectedRoomId = nil;
}
}
- (BOOL)isPresentingMediaPicker {
return nil != mediaPicker;
}
- (void)presentMediaPicker:(UIImagePickerController*)aMediaPicker {
[self dismissMediaPicker];
[self presentViewController:aMediaPicker animated:YES completion:^{
mediaPicker = aMediaPicker;
}];
}
- (void)dismissMediaPicker {
if (mediaPicker) {
[self dismissViewControllerAnimated:NO completion:nil];
mediaPicker.delegate = nil;
mediaPicker = nil;
}
}
- (void)setVisibleRoomId:(NSString *)aVisibleRoomId {
[[MatrixSDKHandler sharedHandler] restoreInAppNotificationsForRoomId:aVisibleRoomId];
_visibleRoomId = aVisibleRoomId;
}
@end
@@ -0,0 +1,27 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
#import "MatrixSDKHandler.h"
@interface MemberViewController : MXCTableViewController
@property (strong, nonatomic) MXRoomMember *mxRoomMember;
@property (strong, nonatomic) MXRoom *mxRoom;
@end
@@ -0,0 +1,507 @@
/*
Copyright 2014 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 "MemberViewController.h"
#import "AppDelegate.h"
#import "RoomMemberActionsCell.h"
#import "MediaManager.h"
@interface MemberViewController () {
NSString *thumbnailURL;
MediaLoader* imageLoader;
id membersListener;
NSMutableArray* buttonsTitles;
// mask view while processing a request
UIView* pendingRequestMask;
UIActivityIndicatorView * pendingMaskSpinnerView;
}
// graphical objects
@property (strong, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIButton *memberThumbnailButton;
@property (weak, nonatomic) IBOutlet UITextView *roomMemberMID;
@property (strong, nonatomic) MXCAlert *actionMenu;
- (IBAction)onButtonToggle:(id)sender;
@end
@implementation MemberViewController
@synthesize mxRoom;
- (void)dealloc {
// close any pending actionsheet
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (imageLoader) {
[imageLoader cancel];
imageLoader = nil;
}
if (membersListener) {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[mxHandler.mxSession removeListener:membersListener];
membersListener = nil;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// remove the line separator color
self.tableView.separatorColor = [UIColor clearColor];
self.tableView.rowHeight = 44;
self.tableView.allowsSelection = NO;
buttonsTitles = [[NSMutableArray alloc] init];
// ignore useless update
if (_mxRoomMember) {
[self updateMemberInfo];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
NSArray *mxMembersEvents = @[
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomPowerLevels
];
// list on member updates
membersListener = [mxHandler.mxSession listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// consider only live event
if (direction == MXEventDirectionForwards) {
// Check the room Id (if any)
if (event.roomId && [event.roomId isEqualToString:mxRoom.state.roomId] == NO) {
// This event does not concern the current room members
return;
}
// Hide potential action sheet
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
MXRoomMember* nextRoomMember = nil;
// get the updated memmber
NSArray* membersList = [self.mxRoom.state members];
for (MXRoomMember* member in membersList) {
if ([member.userId isEqual:_mxRoomMember.userId]) {
nextRoomMember = member;
break;
}
}
// does the member still exist ?
if (nextRoomMember) {
// Refresh members list
_mxRoomMember = nextRoomMember;
[self updateMemberInfo];
[self.tableView reloadData];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController popToRootViewControllerAnimated:NO];
[[AppDelegate theDelegate].masterTabBarController setVisibleRoomId:nil];
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:YES];
});
}
}
}];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (imageLoader) {
[imageLoader cancel];
imageLoader = nil;
}
if (membersListener) {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[mxHandler.mxSession removeListener:membersListener];
membersListener = nil;
}
}
- (void)updateMemberInfo {
self.title = _mxRoomMember.displayname ? _mxRoomMember.displayname : _mxRoomMember.userId;
// set the thumbnail info
[[self.memberThumbnailButton imageView] setContentMode: UIViewContentModeScaleAspectFill];
[[self.memberThumbnailButton imageView] setClipsToBounds:YES];
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (_mxRoomMember.avatarUrl) {
// Suppose this url is a matrix content uri, we use SDK to get the well adapted thumbnail from server
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
thumbnailURL = [mxHandler thumbnailURLForContent:_mxRoomMember.avatarUrl inViewSize:self.memberThumbnailButton.frame.size withMethod:MXThumbnailingMethodCrop];
// Check whether the image download is in progress
id loader = [MediaManager existingDownloaderForURL:thumbnailURL inFolder:kMediaManagerThumbnailFolder];
if (loader) {
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
} else {
// Retrieve the image from cache
UIImage* image = [MediaManager loadCachePictureForURL:thumbnailURL inFolder:kMediaManagerThumbnailFolder];
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
} else {
// Cancel potential download in progress
if (imageLoader) {
[imageLoader cancel];
}
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
imageLoader = [MediaManager downloadMediaFromURL:thumbnailURL withType:@"image/jpeg" inFolder:kMediaManagerThumbnailFolder];
}
}
} else {
UIImage *image = [UIImage imageNamed:@"default-profile"];
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
}
}
self.roomMemberMID.text = _mxRoomMember.userId;
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:thumbnailURL]) {
// update the image
UIImage* image = [MediaManager loadCachePictureForURL:thumbnailURL inFolder:kMediaManagerThumbnailFolder];
if (image == nil) {
image = [UIImage imageNamed:@"default-profile"];
}
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
}
// remove the observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
imageLoader = nil;
}
}
}
- (void)setRoomMember:(MXRoomMember*) aRoomMember {
// ignore useless update
if (![_mxRoomMember.userId isEqualToString:aRoomMember.userId]) {
_mxRoomMember = aRoomMember;
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
// Check user's power level before allowing an action (kick, ban, ...)
MXRoomPowerLevels *powerLevels = [mxRoom.state powerLevels];
NSUInteger memberPowerLevel = [powerLevels powerLevelOfUserWithUserID:_mxRoomMember.userId];
NSUInteger oneSelfPowerLevel = [powerLevels powerLevelOfUserWithUserID:mxHandler.userId];
[buttonsTitles removeAllObjects];
// Consider the case of the user himself
if ([_mxRoomMember.userId isEqualToString:mxHandler.userId]) {
[buttonsTitles addObject:@"Leave"];
if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomPowerLevels]) {
[buttonsTitles addObject:@"Set Power Level"];
}
} else {
// Consider membership of the selected member
switch (_mxRoomMember.membership) {
case MXMembershipInvite:
case MXMembershipJoin: {
// Check conditions to be able to kick someone
if (oneSelfPowerLevel >= [powerLevels kick] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Kick"];
}
// Check conditions to be able to ban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Ban"];
}
break;
}
case MXMembershipLeave: {
// Check conditions to be able to invite someone
if (oneSelfPowerLevel >= [powerLevels invite]) {
[buttonsTitles addObject:@"Invite"];
}
// Check conditions to be able to ban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Ban"];
}
break;
}
case MXMembershipBan: {
// Check conditions to be able to unban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Unban"];
}
break;
}
default: {
break;
}
}
// update power level
if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomPowerLevels]) {
[buttonsTitles addObject:@"Set Power Level"];
}
// offer to start a new chat only if the room is not a 1:1 room with this user
// it does not make sense : it would open the same room
NSString* roomId = [mxHandler privateOneToOneRoomIdWithUserId:_mxRoomMember.userId];
if (![roomId isEqualToString:mxRoom.state.roomId]) {
[buttonsTitles addObject:@"Start Chat"];
}
}
return (buttonsTitles.count + 1) / 2;
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.tableView == aTableView) {
NSInteger row = indexPath.row;
RoomMemberActionsCell* memberActionsCell = (RoomMemberActionsCell*)[aTableView dequeueReusableCellWithIdentifier:@"MemberActionsCell" forIndexPath:indexPath];
NSString* leftTitle = nil;
NSString* rightTitle = nil;
if ((row * 2) < buttonsTitles.count) {
leftTitle = [buttonsTitles objectAtIndex:row * 2];
}
if (((row * 2) + 1) < buttonsTitles.count) {
rightTitle = [buttonsTitles objectAtIndex:(row * 2) + 1];
}
[memberActionsCell setLeftButtonText:leftTitle];
[memberActionsCell setRightButtonText:rightTitle];
return memberActionsCell;
}
return nil;
}
#pragma mark - button management
- (BOOL)hasPendingAction {
return nil != pendingMaskSpinnerView;
}
- (void) addPendingActionMask {
// add a spinner above the tableview to avoid that the user tap on any other button
pendingMaskSpinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.5];
pendingMaskSpinnerView.frame = self.tableView.frame;
pendingMaskSpinnerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
// append it
[self.tableView.superview addSubview:pendingMaskSpinnerView];
// animate it
[pendingMaskSpinnerView startAnimating];
}
- (void) removePendingActionMask {
if (pendingMaskSpinnerView) {
[pendingMaskSpinnerView removeFromSuperview];
pendingMaskSpinnerView = nil;
[self.tableView reloadData];
}
}
- (void) setUserPowerLevel:(MXRoomMember*)roomMember to:(int)value {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
int currentPowerLevel = (int)([mxHandler getPowerLevel:roomMember inRoom:self.mxRoom] * 100);
// check if the power level has not yet been set to 0
if (value != currentPowerLevel) {
__weak typeof(self) weakSelf = self;
[weakSelf addPendingActionMask];
// Reset user power level
[self.mxRoom setPowerLevelOfUserWithUserID:roomMember.userId powerLevel:value success:^{
[weakSelf removePendingActionMask];
} failure:^(NSError *error) {
[weakSelf removePendingActionMask];
NSLog(@"[MemberVC] Set user power (%@) failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
- (void) updateUserPowerLevel:(MXRoomMember*)roomMember {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
__weak typeof(self) weakSelf = self;
// Ask for userId to invite
self.actionMenu = [[MXCAlert alloc] initWithTitle:@"Power Level" message:nil style:MXCAlertStyleAlert];
if (![mxHandler.userId isEqualToString:roomMember.userId]) {
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Reset to default" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
weakSelf.actionMenu = nil;
[weakSelf setUserPowerLevel:roomMember to:0];
}];
}
[self.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.text = [NSString stringWithFormat:@"%d", (int)([mxHandler getPowerLevel:roomMember inRoom:weakSelf.mxRoom] * 100)];
textField.placeholder = nil;
textField.keyboardType = UIKeyboardTypeDecimalPad;
}];
[self.actionMenu addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
UITextField *textField = [alert textFieldAtIndex:0];
weakSelf.actionMenu = nil;
if (textField.text.length > 0) {
[weakSelf setUserPowerLevel:roomMember to:(int)[textField.text integerValue]];
}
}];
[self.actionMenu showInViewController:self];
}
- (IBAction)onButtonToggle:(id)sender {
if ([sender isKindOfClass:[UIButton class]]) {
// already a pending action
if ([self hasPendingAction]) {
return;
}
NSString* text = ((UIButton*)sender).titleLabel.text;
if ([text isEqualToString:@"Leave"]) {
[self addPendingActionMask];
[self.mxRoom leave:^{
[self removePendingActionMask];
[self.navigationController popToRootViewControllerAnimated:NO];
[[AppDelegate theDelegate].masterTabBarController setVisibleRoomId:nil];
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:YES];
} failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"[MemberVC] Leave room %@ failed: %@", mxRoom.state.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Set Power Level"]) {
[self updateUserPowerLevel:_mxRoomMember];
} else if ([text isEqualToString:@"Kick"]) {
[self addPendingActionMask];
[mxRoom kickUser:_mxRoomMember.userId
reason:nil
success:^{
[self removePendingActionMask];
[self.navigationController popToRootViewControllerAnimated:YES];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"[MemberVC] Kick %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Ban"]) {
[self addPendingActionMask];
[mxRoom banUser:_mxRoomMember.userId
reason:nil
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"[MemberVC] Ban %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Invite"]) {
[self addPendingActionMask];
[mxRoom inviteUser:_mxRoomMember.userId
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"[MemberVC] Invite %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Unban"]) {
[self addPendingActionMask];
[mxRoom unbanUser:_mxRoomMember.userId
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"[MemberVC] Unban %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Start Chat"]) {
[self addPendingActionMask];
[[MatrixSDKHandler sharedHandler] startPrivateOneToOneRoomWithUserId:_mxRoomMember.userId];
}
}
}
@end
@@ -0,0 +1,26 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
@class RoomViewController;
@interface RecentsViewController : MXCTableViewController <UISearchBarDelegate>
@property (strong, nonatomic) NSString *preSelectedRoomId; // set a non-nil value to this property will open room details
@end
@@ -0,0 +1,757 @@
/*
Copyright 2014 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 "RecentsViewController.h"
#import "RoomViewController.h"
#import "RecentRoom.h"
#import "RecentsTableViewCell.h"
#import "AppDelegate.h"
#import "MatrixSDKHandler.h"
#import "MediaManager.h"
@interface RecentsViewController () {
// Array of RecentRooms
NSMutableArray *recents;
id recentsListener;
NSUInteger unreadCount;
// Search
UISearchBar *recentsSearchBar;
NSMutableArray *filteredRecents;
BOOL searchBarShouldEndEditing;
// Date formatter
NSDateFormatter *dateFormatter;
// Keep reference on the current room view controller to release it correctly
RoomViewController *currentRoomViewController;
// Keep the selected cell index to handle correctly split view controller display in landscape mode
NSInteger currentSelectedCellIndexPathRow;
// The activity indicator is displayed on main screen in order to ignore potential table scrolling
// In some case this activity indicator shoud be hidden (For example when the recents view controller is not visible).
BOOL shouldHideActivityIndicator;
}
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@end
@implementation RecentsViewController
- (void)awakeFromNib {
[super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.preferredContentSize = CGSizeMake(320.0, 600.0);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem;
UIBarButtonItem *searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(createNewRoom:)];
self.navigationItem.rightBarButtonItems = @[searchButton, addButton];
// Add background to activity indicator
CGRect frame = _activityIndicator.frame;
frame.size.width += 30;
frame.size.height += 30;
_activityIndicator.bounds = frame;
_activityIndicator.backgroundColor = [UIColor darkGrayColor];
[_activityIndicator.layer setCornerRadius:5];
// Initialisation
recents = nil;
filteredRecents = nil;
unreadCount = 0;
currentSelectedCellIndexPathRow = -1;
NSString *dateFormat = @"MMM dd HH:mm";
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setDateFormat:dateFormat];
[[MatrixSDKHandler sharedHandler] addObserver:self forKeyPath:@"status" options:0 context:nil];
}
- (void)dealloc {
if (currentRoomViewController) {
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
if (recentsListener) {
[[MatrixSDKHandler sharedHandler].mxSession removeListener:recentsListener];
recentsListener = nil;
}
recents = nil;
_preSelectedRoomId = nil;
recentsSearchBar = nil;
filteredRecents = nil;
if (dateFormatter) {
dateFormatter = nil;
}
[[MatrixSDKHandler sharedHandler] removeObserver:self forKeyPath:@"status"];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[mxHandler addObserver:self forKeyPath:@"isActivityInProgress" options:0 context:nil];
// Refresh display
shouldHideActivityIndicator = NO;
if (mxHandler.isActivityInProgress) {
[self startActivityIndicator];
}
[self configureView];
if (self.splitViewController) {
// Deselect the current selected row, it will be restored on viewDidAppear (if any)
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
if (indexPath) {
[self.tableView deselectRowAtIndexPath:indexPath animated:NO];
}
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[MatrixSDKHandler sharedHandler] removeObserver:self forKeyPath:@"isActivityInProgress"];
// Leave potential editing mode
[self setEditing:NO];
// Leave potential search session
if (recentsSearchBar) {
[self searchBarCancelButtonClicked:recentsSearchBar];
}
// Hide activity indicator
[self stopActivityIndicator];
shouldHideActivityIndicator = YES;
_preSelectedRoomId = nil;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Release the current selected room (if any) except if the Room ViewController is still visible (see splitViewController.isCollapsed condition)
if (!self.splitViewController || self.splitViewController.isCollapsed) {
if (currentRoomViewController) {
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
// Reset selected row index
currentSelectedCellIndexPathRow = -1;
}
} else {
// In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen,
// the selected room (if any) is highlighted.
[self refreshCurrentSelectedCell:YES];
}
}
#pragma mark -
- (void)setPreSelectedRoomId:(NSString *)roomId {
_preSelectedRoomId = nil;
if (roomId) {
// Check whether recents update is in progress
if ([_activityIndicator isAnimating]) {
// Postpone room details display
_preSelectedRoomId = roomId;
return;
}
// Look for the room index in recents list
NSIndexPath *indexPath = nil;
for (NSUInteger index = 0; index < recents.count; index++) {
RecentRoom *recentRoom = [recents objectAtIndex:index];
if ([roomId isEqualToString:recentRoom.roomId]) {
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
}
if (indexPath) {
// Open details view
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
UITableViewCell *recentCell = [self.tableView cellForRowAtIndexPath:indexPath];
[self performSegueWithIdentifier:@"showDetail" sender:recentCell];
} else {
NSLog(@"[RecentsVC] We are not able to open room (%@) because it does not appear in recents yet", roomId);
// Postpone room details display. We run activity indicator until recents are updated (thanks to recents listener)
_preSelectedRoomId = roomId;
// Start activity indicator
[self startActivityIndicator];
}
} else if (currentRoomViewController) {
// Release the current selected room
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
// Force table refresh to deselect related cell
[self refreshRecentsDisplay];
}
}
#pragma mark - Internal methods
- (void)refreshRecentsDisplay {
// Check whether the current selected room has not been left
if (currentRoomViewController.roomId) {
MXRoom *mxRoom = [[MatrixSDKHandler sharedHandler].mxSession roomWithRoomId:currentRoomViewController.roomId];
if (mxRoom == nil || mxRoom.state.membership == MXMembershipLeave || mxRoom.state.membership == MXMembershipBan) {
// release the room viewController
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
}
[self.tableView reloadData];
// In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen,
// the selected room (if any) is updated and kept visible.
if (self.splitViewController && !self.splitViewController.isCollapsed) {
[self refreshCurrentSelectedCell:YES];
}
}
- (void)configureView {
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kRecentRoomUpdatedByBackPagination object:nil];
if (mxHandler.mxSession) {
// Check matrix handler status
if (mxHandler.status == MatrixSDKHandlerStatusStoreDataReady || mxHandler.status == MatrixSDKHandlerStatusInitialServerSyncInProgress) {
// Server sync is not complete yet
if (!recents) {
// Retrieve recents from local storage (some data may not be up-to-date)
NSArray *recentEvents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
recents = [NSMutableArray arrayWithCapacity:recentEvents.count];
for (MXEvent *mxEvent in recentEvents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:mxEvent.roomId];
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:mxEvent andRoomState:mxRoom.state markAsUnread:NO];
if (recentRoom) {
[recents addObject:recentRoom];
}
}
unreadCount = 0;
}
} else if (mxHandler.status == MatrixSDKHandlerStatusServerSyncDone) {
// Force recents refresh and add listener to update them (if it is not already done)
if (!recentsListener) {
NSArray *recentEvents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
recents = [NSMutableArray arrayWithCapacity:recentEvents.count];
for (MXEvent *mxEvent in recentEvents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:mxEvent.roomId];
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:mxEvent andRoomState:mxRoom.state markAsUnread:NO];
if (recentRoom) {
[recents addObject:recentRoom];
}
}
unreadCount = 0;
// Check whether redaction event belongs to the listened events list
NSArray *listenedEventTypes = mxHandler.eventsFilterForMessages;
BOOL hideRedactionEvent = ([listenedEventTypes indexOfObject:kMXEventTypeStringRoomRedaction] == NSNotFound);
if (hideRedactionEvent) {
// Add redaction event to the listened events list in order to take into account redaction of the last event in recents.
// (See [RecentRoom updateWithLastEvent:...] for more details)
listenedEventTypes = [listenedEventTypes arrayByAddingObject:kMXEventTypeStringRoomRedaction];
}
// Register recent listener
recentsListener = [mxHandler.mxSession listenToEventsOfTypes:listenedEventTypes onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
// Consider first live event
if (direction == MXEventDirectionForwards) {
// Check user's membership in live room state (We will remove left rooms from recents)
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:event.roomId];
BOOL isLeft = (mxRoom == nil || mxRoom.state.membership == MXMembershipLeave || mxRoom.state.membership == MXMembershipBan);
// Consider this new event as unread only if the sender is not the user and if the room is not visible
BOOL isUnread = (![event.userId isEqualToString:mxHandler.userId]
&& ![[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId]);
// Look for the room
BOOL isFound = NO;
for (NSUInteger index = 0; index < recents.count; index++) {
RecentRoom *recentRoom = [recents objectAtIndex:index];
if ([event.roomId isEqualToString:recentRoom.roomId]) {
isFound = YES;
// Decrement here unreads count for this recent (we will add later the refreshed count)
unreadCount -= recentRoom.unreadCount;
if (isLeft) {
// Remove left room
[recents removeObjectAtIndex:index];
if (filteredRecents) {
NSUInteger filteredIndex = [filteredRecents indexOfObject:recentRoom];
if (filteredIndex != NSNotFound) {
[filteredRecents removeObjectAtIndex:filteredIndex];
}
}
} else {
if ([recentRoom updateWithLastEvent:event andRoomState:roomState markAsUnread:isUnread]) {
if (index) {
// Move this room at first position
[recents removeObjectAtIndex:index];
[recents insertObject:recentRoom atIndex:0];
}
// Update filtered recents (if any)
if (filteredRecents) {
NSUInteger filteredIndex = [filteredRecents indexOfObject:recentRoom];
if (filteredIndex && filteredIndex != NSNotFound) {
[filteredRecents removeObjectAtIndex:filteredIndex];
[filteredRecents insertObject:recentRoom atIndex:0];
}
}
}
// Refresh global unreads count
unreadCount += recentRoom.unreadCount;
}
// Refresh title
[self updateTitleView];
break;
}
}
if (!isFound && !isLeft) {
// Insert in first position this new room
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:event andRoomState:roomState markAsUnread:isUnread];
if (recentRoom) {
[recents insertObject:recentRoom atIndex:0];
if (isUnread) {
unreadCount++;
[self updateTitleView];
}
// Check whether we were waiting for this room
if (_preSelectedRoomId) {
if ([recentRoom.roomId isEqualToString:_preSelectedRoomId]) {
[self stopActivityIndicator];
self.preSelectedRoomId = _preSelectedRoomId;
}
}
}
}
// Reload table
[self refreshRecentsDisplay];
}
}];
}
// else nothing to do
} else if (mxHandler.status != MatrixSDKHandlerStatusPaused) {
// Here status is MatrixSDKHandlerStatusLoggedOut or MatrixSDKHandlerStatusLogged - Reset recents
recents = nil;
}
// Reload table
[self refreshRecentsDisplay];
// Check whether a room is preselected
if (_preSelectedRoomId) {
self.preSelectedRoomId = _preSelectedRoomId;
}
} else {
if (mxHandler.status == MatrixSDKHandlerStatusLoggedOut) {
// Update title
unreadCount = 0;
[self updateTitleView];
}
recents = nil;
[self refreshRecentsDisplay];
}
if (recents) {
// Add observer to force refresh when a recent last description is updated thanks to back pagination
// (This happens when the current last event description is blank, a back pagination is triggered to display non empty description)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onRecentRoomUpdatedByBackPagination:) name:kRecentRoomUpdatedByBackPagination object:nil];
} else {
// Remove potential listener
if (recentsListener && mxHandler.mxSession) {
[mxHandler.mxSession removeListener:recentsListener];
recentsListener = nil;
}
}
[self updateTitleView];
}
- (void)onRecentRoomUpdatedByBackPagination:(NSNotification *)notif{
[self refreshRecentsDisplay];
[self updateTitleView];
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* roomId = notif.object;
// Check whether this room is currently displayed in RoomViewController
if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:roomId]) {
// For sanity reason, we have to force a full refresh in order to restore back state of the room
dispatch_async(dispatch_get_main_queue(), ^{
[currentRoomViewController forceRefresh];
});
}
}
}
- (void)updateTitleView {
NSString *title = @"Recents";
if (unreadCount) {
title = [NSString stringWithFormat:@"Recents (%tu)", unreadCount];
}
self.navigationItem.title = title;
}
- (void)createNewRoom:(id)sender {
[[AppDelegate theDelegate].masterTabBarController showRoomCreationForm];
}
- (void)search:(id)sender {
if (!recentsSearchBar) {
// Check whether there are data in which search
if (recents.count) {
// Create search bar
recentsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
recentsSearchBar.showsCancelButton = YES;
recentsSearchBar.returnKeyType = UIReturnKeyDone;
recentsSearchBar.delegate = self;
searchBarShouldEndEditing = NO;
// add it to the tableHeaderView
// do not create a header view
// the header view is refreshed every time there is a [tableView reloaddata]
// i.e. there is a removeFromSuperView call, the view is added to the tableview..
// with a first respondable view, IOS seems lost to find the first responder
// so, the keyboard is always displayed and can not be dismissed
// tableHeaderView is never removed from superview so the first responder is not lost
self.tableView.tableHeaderView = recentsSearchBar;
[recentsSearchBar becomeFirstResponder];
[self scrollToTop];
}
} else {
[self searchBarCancelButtonClicked: recentsSearchBar];
}
}
- (void)startActivityIndicator {
// Add the spinner on main screen in order to ignore potential table scrolling
_activityIndicator.center = CGPointMake(self.view.center.x, self.view.center.x);
[[AppDelegate theDelegate].window addSubview:_activityIndicator];
[_activityIndicator startAnimating];
}
- (void)stopActivityIndicator {
[_activityIndicator stopAnimating];
[_activityIndicator removeFromSuperview];
}
- (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];
}
- (void)refreshCurrentSelectedCell:(BOOL)forceVisible {
// Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller.
currentSelectedCellIndexPathRow = -1;
if (currentRoomViewController) {
// Look for the rank of this selected room in displayed recents
NSArray *displayedRecents = filteredRecents ? filteredRecents : recents;
for (NSInteger index = 0; index < displayedRecents.count; index ++) {
RecentRoom *recentRoom = [displayedRecents objectAtIndex:index];
if ([currentRoomViewController.roomId isEqualToString:recentRoom.roomId]) {
currentSelectedCellIndexPathRow = index;
break;
}
}
}
if (currentSelectedCellIndexPathRow != -1) {
// Select the right row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentSelectedCellIndexPathRow inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
if (forceVisible) {
// Scroll table view to make the selected row appear at second position
NSInteger topCellIndexPathRow = currentSelectedCellIndexPathRow ? currentSelectedCellIndexPathRow - 1: currentSelectedCellIndexPathRow;
indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
} else {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
if (indexPath) {
[self.tableView deselectRowAtIndexPath:indexPath animated:NO];
}
}
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"status" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
} else if ([@"isActivityInProgress" isEqualToString:keyPath]) {
if (!shouldHideActivityIndicator && [MatrixSDKHandler sharedHandler].isActivityInProgress) {
[self startActivityIndicator];
} else {
[self stopActivityIndicator];
}
}
}
#pragma mark - Segues
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
RecentRoom *recentRoom;
if (filteredRecents) {
recentRoom = filteredRecents[indexPath.row];
} else {
recentRoom = recents[indexPath.row];
}
UIViewController *controller;
if ([[segue destinationViewController] isKindOfClass:[UINavigationController class]]) {
controller = [[segue destinationViewController] topViewController];
} else {
controller = [segue destinationViewController];
}
if ([controller isKindOfClass:[RoomViewController class]]) {
// Release potential Room ViewController
if (currentRoomViewController) {
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
}
currentRoomViewController = (RoomViewController *)controller;
currentRoomViewController.roomId = recentRoom.roomId;
}
// Reset unread count for this room
unreadCount -= recentRoom.unreadCount;
[recentRoom resetUnreadCount];
[self updateTitleView];
if (self.splitViewController) {
// Refresh selected cell without scrolling the selected cell (We suppose it's visible here)
[self refreshCurrentSelectedCell:NO];
// IOS >= 8
if ([self.splitViewController respondsToSelector:@selector(displayModeButtonItem)]) {
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
}
// hide the keyboard when opening a new controller
// do not hide the searchBar until the RecentsViewController is dismissed
// on tablets / iphone 6+, the user could expect to search again while looking at a room
if ([recentsSearchBar isFirstResponder]) {
searchBarShouldEndEditing = YES;
[recentsSearchBar resignFirstResponder];
}
//
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
// Hide back button title
self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
}
}
#pragma mark - Table View
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (filteredRecents) {
return filteredRecents.count;
}
return recents.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 70;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecentsTableViewCell *cell = (RecentsTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"RecentsCell" forIndexPath:indexPath];
RecentRoom *recentRoom;
if (filteredRecents) {
recentRoom = filteredRecents[indexPath.row];
} else {
recentRoom = recents[indexPath.row];
}
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:recentRoom.roomId];
cell.roomTitle.text = [mxRoom.state displayname];
cell.lastEventDescription.text = recentRoom.lastEventDescription;
// Set in bold public room name
if (mxRoom.state.isPublic) {
cell.roomTitle.font = [UIFont boldSystemFontOfSize:20];
} else {
cell.roomTitle.font = [UIFont systemFontOfSize:19];
}
if (recentRoom.lastEventOriginServerTs != kMXUndefinedTimestamp) {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:recentRoom.lastEventOriginServerTs/1000];
cell.recentDate.text = [dateFormatter stringFromDate:date];
} else {
cell.recentDate.text = nil;
}
// Set background color
if (recentRoom.unreadCount) {
if (recentRoom.containsBingUnread) {
cell.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:1 alpha:1.0];
} else {
cell.backgroundColor = [UIColor colorWithRed:1 green:0.9 blue:0.9 alpha:1.0];
}
cell.roomTitle.text = [NSString stringWithFormat:@"%@ (%tu)", cell.roomTitle.text, recentRoom.unreadCount];
} else {
cell.backgroundColor = [UIColor clearColor];
}
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Leave the selected room
RecentRoom *selectedRoom;
if (filteredRecents) {
selectedRoom = filteredRecents[indexPath.row];
} else {
selectedRoom = recents[indexPath.row];
}
MXRoom *mxRoom = [[MatrixSDKHandler sharedHandler].mxSession roomWithRoomId:selectedRoom.roomId];
// cancel pending uploads/downloads
// they are useless by now
[MediaManager cancelDownloadsInFolder:selectedRoom.roomId];
[MediaManager cancelUploadsInFolder:selectedRoom.roomId];
[mxRoom leave:^{
// Remove the selected room (if it is not already done by recents listener)
for (NSUInteger index = 0; index < recents.count; index++) {
RecentRoom *recentRoom = [recents objectAtIndex:index];
if ([recentRoom.roomId isEqualToString:selectedRoom.roomId]) {
[recents removeObjectAtIndex:index];
if (filteredRecents) {
NSUInteger filteredIndex = [filteredRecents indexOfObject:selectedRoom];
if (filteredIndex != NSNotFound) {
[filteredRecents removeObjectAtIndex:filteredIndex];
}
}
break;
}
}
// Refresh table display
[self refreshRecentsDisplay];
} failure:^(NSError *error) {
NSLog(@"[RecentsVC] Failed to leave room (%@) failed: %@", selectedRoom.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
#pragma mark - UISearchBarDelegate
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
searchBarShouldEndEditing = NO;
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar {
return searchBarShouldEndEditing;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
// Update filtered list
if (searchText.length) {
if (filteredRecents) {
[filteredRecents removeAllObjects];
} else {
filteredRecents = [NSMutableArray arrayWithCapacity:recents.count];
}
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
for (RecentRoom *recentRoom in recents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:recentRoom.roomId];
if ([[mxRoom.state displayname] rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[filteredRecents addObject:recentRoom];
}
}
} else {
filteredRecents = nil;
}
// Refresh display
[self refreshRecentsDisplay];
[self scrollToTop];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// "Done" key has been pressed
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
// Leave search
searchBarShouldEndEditing = YES;
[searchBar resignFirstResponder];
recentsSearchBar = nil;
filteredRecents = nil;
self.tableView.tableHeaderView = nil;
[self refreshRecentsDisplay];
[self scrollToTop];
}
@end
@@ -0,0 +1,28 @@
/*
Copyright 2014 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 "MXCViewController.h"
#import "HPGrowingTextView.h"
@interface RoomViewController : MXCViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate, HPGrowingTextViewDelegate>
@property (strong, nonatomic) NSString *roomId;
- (void)forceRefresh;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
/*
Copyright 2014 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 "MXCTableViewController.h"
@interface SettingsViewController : MXCTableViewController <UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate>
typedef void (^blockSettings_onReadyToLeave)();
- (void)reset;
- (BOOL)shouldLeave:(blockSettings_onReadyToLeave)handler;
@end
File diff suppressed because it is too large Load Diff