diff --git a/Vector.xcodeproj/project.pbxproj b/Vector.xcodeproj/project.pbxproj index a5248d387..08aa1c8b8 100644 --- a/Vector.xcodeproj/project.pbxproj +++ b/Vector.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ F094AA321B78E42600B1FBBF /* GlobalNotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA1D1B78E42600B1FBBF /* GlobalNotificationSettingsViewController.m */; }; F094AA341B78E42600B1FBBF /* MasterTabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA211B78E42600B1FBBF /* MasterTabBarController.m */; }; F094AA351B78E42600B1FBBF /* RecentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA231B78E42600B1FBBF /* RecentsViewController.m */; }; - F094AA361B78E42600B1FBBF /* RoomMembersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA251B78E42600B1FBBF /* RoomMembersViewController.m */; }; F094AA371B78E42600B1FBBF /* RoomViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA271B78E42600B1FBBF /* RoomViewController.m */; }; F094AA381B78E42600B1FBBF /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F094AA291B78E42600B1FBBF /* SettingsViewController.m */; }; F0A1CD221B9F4BBA00F9C15C /* RoomParticipantsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0A1CD211B9F4BBA00F9C15C /* RoomParticipantsViewController.m */; }; @@ -173,8 +172,6 @@ F094AA211B78E42600B1FBBF /* MasterTabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MasterTabBarController.m; sourceTree = ""; }; F094AA221B78E42600B1FBBF /* RecentsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentsViewController.h; sourceTree = ""; }; F094AA231B78E42600B1FBBF /* RecentsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RecentsViewController.m; sourceTree = ""; }; - F094AA241B78E42600B1FBBF /* RoomMembersViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMembersViewController.h; sourceTree = ""; }; - F094AA251B78E42600B1FBBF /* RoomMembersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMembersViewController.m; sourceTree = ""; }; F094AA261B78E42600B1FBBF /* RoomViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomViewController.h; sourceTree = ""; }; F094AA271B78E42600B1FBBF /* RoomViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomViewController.m; sourceTree = ""; }; F094AA281B78E42600B1FBBF /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; @@ -434,8 +431,6 @@ F0DD7D811B7B363300C4BE02 /* RoomCreationStep1ViewController.m */, F0DD7D861B7B507100C4BE02 /* RoomCreationStep2ViewController.h */, F0DD7D871B7B507100C4BE02 /* RoomCreationStep2ViewController.m */, - F094AA241B78E42600B1FBBF /* RoomMembersViewController.h */, - F094AA251B78E42600B1FBBF /* RoomMembersViewController.m */, F094AA261B78E42600B1FBBF /* RoomViewController.h */, F094AA271B78E42600B1FBBF /* RoomViewController.m */, F094AA281B78E42600B1FBBF /* SettingsViewController.h */, @@ -688,7 +683,6 @@ F094AA321B78E42600B1FBBF /* GlobalNotificationSettingsViewController.m in Sources */, F094A9A81B78D8F000B1FBBF /* main.m in Sources */, F0DD7D881B7B507100C4BE02 /* RoomCreationStep2ViewController.m in Sources */, - F094AA361B78E42600B1FBBF /* RoomMembersViewController.m in Sources */, F094AA2D1B78E42600B1FBBF /* RecentListDataSource.m in Sources */, F094AA2A1B78E42600B1FBBF /* RageShakeManager.m in Sources */, F094AA351B78E42600B1FBBF /* RecentsViewController.m in Sources */, diff --git a/Vector/Assets/en.lproj/Vector.strings b/Vector/Assets/en.lproj/Vector.strings index 1c2dc298b..ec090f895 100644 --- a/Vector/Assets/en.lproj/Vector.strings +++ b/Vector/Assets/en.lproj/Vector.strings @@ -24,6 +24,8 @@ "next" = "Next"; "back" = "Back"; "create" = "Create"; +"leave" = "Leave"; +"remove" = "Remove"; // Chat creation "room_creation_title" = "New Chat"; @@ -45,6 +47,11 @@ "room_participants_add_participant" = "Add participant"; "room_participants_one_participant" = "1 participant"; "room_participants_multi_participants" = "%d participants"; +"room_participants_leave_prompt_title" = "Leave?"; +"room_participants_leave_prompt_msg" = "Are you sure you want to leave this chat?"; +"room_participants_remove_prompt_title" = "Remove?"; +"room_participants_remove_prompt_msg" = "Are you sure you want to remove %@ from this chat?"; +"room_participants_admin_name" = "%@ (admin)"; // Chat "room_menu_search" = "Search"; diff --git a/Vector/Base.lproj/Main.storyboard b/Vector/Base.lproj/Main.storyboard index 2852c39f7..f71f528f0 100644 --- a/Vector/Base.lproj/Main.storyboard +++ b/Vector/Base.lproj/Main.storyboard @@ -46,8 +46,7 @@ - - + @@ -225,11 +224,11 @@ - + - - + + @@ -238,22 +237,11 @@ - - - - - - - - - - - @@ -472,7 +460,6 @@ - diff --git a/Vector/ViewController/RoomCreationStep2ViewController.m b/Vector/ViewController/RoomCreationStep2ViewController.m index 1a85166d9..31da39534 100644 --- a/Vector/ViewController/RoomCreationStep2ViewController.m +++ b/Vector/ViewController/RoomCreationStep2ViewController.m @@ -65,7 +65,7 @@ [super viewWillDisappear:animated]; // Update the selected participants - _roomCreationInputs.roomParticipants = self.roomParticipants; + _roomCreationInputs.roomParticipants = mutableParticipants; // TODO Shall we cancel room creation on Back? Shall we prompt user? Shall we disable back during room creation // [roomCreationRequest cancel]; @@ -80,10 +80,10 @@ if (roomCreationInputs.mxSession) { - self.userMatrixId = roomCreationInputs.mxSession.myUser.userId; + userMatrixId = roomCreationInputs.mxSession.myUser.userId; } - self.roomParticipants = roomCreationInputs.roomParticipants; + mutableParticipants = [NSMutableArray arrayWithArray:roomCreationInputs.roomParticipants]; } #pragma mark - Override RoomParticipantsViewController @@ -102,6 +102,119 @@ super.isAddParticipantSearchBarEditing = isAddParticipantSearchBarEditing; } +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == participantsSection) + { + MXKContactTableCell *participantCell = [tableView dequeueReusableCellWithIdentifier:[MXKContactTableCell defaultReuseIdentifier]]; + if (!participantCell) + { + participantCell = [[MXKContactTableCell alloc] init]; + } + + if (userMatrixId && indexPath.row == 0) + { + MXKContact *contact = [mxkContactsById objectForKey:userMatrixId]; + if (! contact) + { + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:NSLocalizedStringFromTable(@"you", @"Vector", nil) andMatrixID:userMatrixId]; + [mxkContactsById setObject:contact forKey:userMatrixId]; + } + + [participantCell render:contact]; + + // Hide accessory view. + participantCell.contactAccessoryViewType = MXKContactTableCellAccessoryCustom; + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + else + { + NSInteger index = indexPath.row; + + if (userMatrixId) + { + index --; + } + + if (index < mutableParticipants.count) + { + NSString *userId = mutableParticipants[index]; + MXKContact *contact = [mxkContactsById objectForKey:userId]; + if (!contact) + { + // Create this missing contact + // Look for the corresponding MXUser + NSArray *sessions = self.mxSessions; + MXUser *mxUser; + for (MXSession *session in sessions) + { + mxUser = [session userWithUserId:userId]; + if (mxUser) + { + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:((mxUser.displayname.length > 0) ? mxUser.displayname : userId) andMatrixID:userId]; + break; + } + } + + if (contact) + { + [mxkContactsById setObject:contact forKey:userId]; + } + + } + + if (contact) + { + [participantCell render:contact]; + } + + // Show 'remove' icon. + participantCell.contactAccessoryViewType = MXKContactTableCellAccessoryCustom; + participantCell.contactAccessoryViewHeightConstraint.constant = 30; + participantCell.contactAccessoryViewWidthConstraint.constant = 30; + participantCell.contactAccessoryImageView.image = [UIImage imageNamed:@"remove"]; + participantCell.contactAccessoryImageView.hidden = NO; + participantCell.contactAccessoryView.hidden = NO; + + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + } + + return participantCell; + } + + return [super tableView:tableView cellForRowAtIndexPath:indexPath]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == participantsSection) + { + NSInteger index = indexPath.row; + + if (userMatrixId) + { + index --; + } + + if (index < mutableParticipants.count) + { + [mxkContactsById removeObjectForKey:mutableParticipants[index]]; + [mutableParticipants removeObjectAtIndex:index]; + + // Refresh display + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + + UITableViewHeaderFooterView *participantsSectionHeader = [tableView headerViewForSection:participantsSection]; + participantsSectionHeader.textLabel.text = [self tableView:tableView titleForHeaderInSection:participantsSection]; + } + } + else + { + [super tableView:tableView didSelectRowAtIndexPath:indexPath]; + } +} + #pragma mark - Actions - (IBAction)onButtonPressed:(id)sender @@ -113,7 +226,7 @@ [self startActivityIndicator]; // Update the selected participants - _roomCreationInputs.roomParticipants = self.roomParticipants; + _roomCreationInputs.roomParticipants = mutableParticipants; // Create new room roomCreationRequest = [_roomCreationInputs.mxSession createRoom:_roomCreationInputs.roomName diff --git a/Vector/ViewController/RoomMembersViewController.h b/Vector/ViewController/RoomMembersViewController.h deleted file mode 100644 index f923f3086..000000000 --- a/Vector/ViewController/RoomMembersViewController.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - Copyright 2015 OpenMarket Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import - -@interface RoomMembersViewController : MXKRoomMemberListViewController - -@end - diff --git a/Vector/ViewController/RoomMembersViewController.m b/Vector/ViewController/RoomMembersViewController.m deleted file mode 100644 index 2d81c90c7..000000000 --- a/Vector/ViewController/RoomMembersViewController.m +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright 2015 OpenMarket Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "RoomMembersViewController.h" - -#import "AppDelegate.h" -#import "RageShakeManager.h" - -@interface RoomMembersViewController () -{ - /** - The selected member - */ - MXRoomMember *selectedMember; -} - -@end - -@implementation RoomMembersViewController - -- (void)awakeFromNib -{ - [super awakeFromNib]; - - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) - { - self.preferredContentSize = CGSizeMake(320.0, 600.0); - } -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - // Setup `MXKRoomMemberListViewController` properties - self.rageShakeManager = [RageShakeManager sharedManager]; - - // The view controller handles itself the selected roomMember - self.delegate = self; -} - -- (void)viewDidLayoutSubviews -{ - UIEdgeInsets contentInset = self.tableView.contentInset; - contentInset.bottom = self.bottomLayoutGuide.length; - self.tableView.contentInset = contentInset; -} - -- (void)dealloc -{ - selectedMember = nil; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -#pragma mark - Segues - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender -{ - // Keep ref on destinationViewController - [super prepareForSegue:segue sender:sender]; - - if ([[segue identifier] isEqualToString:@"showDetails"]) - { - if (selectedMember) - { - MXKRoomMemberDetailsViewController *memberViewController = segue.destinationViewController; - // Set rageShake handler - memberViewController.rageShakeManager = [RageShakeManager sharedManager]; - // Set delegate to handle start chat option - memberViewController.delegate = [AppDelegate theDelegate]; - - [memberViewController displayRoomMember:selectedMember withMatrixRoom:[self.mainSession roomWithRoomId:self.dataSource.roomId]]; - } - } -} - -#pragma mark - MXKRoomMemberListViewControllerDelegate -- (void)roomMemberListViewController:(MXKRoomMemberListViewController *)roomMemberListViewController didSelectMember:(MXRoomMember *)member -{ - // Report the selected member and open details view - selectedMember = member; - [self performSegueWithIdentifier:@"showDetails" sender:self]; -} - -@end diff --git a/Vector/ViewController/RoomParticipantsViewController.h b/Vector/ViewController/RoomParticipantsViewController.h index f803eccec..92ddaba62 100644 --- a/Vector/ViewController/RoomParticipantsViewController.h +++ b/Vector/ViewController/RoomParticipantsViewController.h @@ -17,16 +17,35 @@ #import @interface RoomParticipantsViewController : MXKTableViewController +{ +@protected + /** + The matrix id of the current user (nil if the user is not a participant of the room). + */ + NSString *userMatrixId; + + /** + Section indexes + */ + NSInteger addParticipantsSection; + NSInteger searchResultSection; + NSInteger participantsSection; + + /** + Mutable list of participants + */ + NSMutableArray *mutableParticipants; + + /** + Store MXKContact instance by matrix user id + */ + NSMutableDictionary *mxkContactsById; +} /** - The matrix id of the current user (nil if the user is not a participant of the room). + A matrix room (nil by default) */ -@property (nonatomic) NSString *userMatrixId; - -/** - The matrix ids of the room participants. The room admin must be at the first position (if different than user). - */ -@property (nonatomic) NSArray *roomParticipants; +@property (nonatomic) MXRoom *mxRoom; /** */ diff --git a/Vector/ViewController/RoomParticipantsViewController.m b/Vector/ViewController/RoomParticipantsViewController.m index 920312c1c..f1f598984 100644 --- a/Vector/ViewController/RoomParticipantsViewController.m +++ b/Vector/ViewController/RoomParticipantsViewController.m @@ -22,20 +22,29 @@ @interface RoomParticipantsViewController () { - NSMutableArray *participants; - - // Add participants - NSInteger addParticipantsSection; + // Add participants section MXKTableViewCellWithSearchBar *addParticipantsSearchBarCell; NSString *addParticipantsSearchText; - // Search result - NSInteger searchResultSection; + // Search result section NSMutableArray *filteredParticipants; - // Participants - NSInteger participantsSection; - NSMutableDictionary *participantsByIds; + MXKAlert *currentAlert; + + /** + Mask view while processing a request + */ + UIActivityIndicatorView * pendingMaskSpinnerView; + + /** + The members events listener. + */ + id membersListener; + + /** + Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room. + */ + id leaveRoomNotificationObserver; } @end @@ -65,6 +74,16 @@ addParticipantsSearchBarCell.mxkSearchBar.returnKeyType = UIReturnKeyDone; addParticipantsSearchBarCell.mxkSearchBar.delegate = self; _isAddParticipantSearchBarEditing = NO; + + if (! mutableParticipants) + { + mutableParticipants = [NSMutableArray array]; + } + + if (! mxkContactsById) + { + mxkContactsById = [NSMutableDictionary dictionary]; + } } - (void)didReceiveMemoryWarning @@ -75,11 +94,27 @@ - (void)destroy { + if (membersListener) + { + [self.mxRoom removeListener:membersListener]; + membersListener = nil; + } + + _mxRoom = nil; + addParticipantsSearchBarCell = nil; filteredParticipants = nil; - participantsByIds = nil; + mxkContactsById = nil; - participants = nil; + mutableParticipants = nil; + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + [self removePendingActionMask]; [super destroy]; } @@ -90,23 +125,112 @@ // Refresh display [self.tableView reloadData]; + + // Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room. + leaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + // Check whether the user will leave the room related to the displayed participants + if (notif.object == self.mxRoom.mxSession) + { + NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey]; + if (roomId && [roomId isEqualToString:self.mxRoom.state.roomId]) + { + // We remove the current view controller. + [self withdrawViewControllerAnimated:YES completion:nil]; + } + } + }]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + if (leaveRoomNotificationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:leaveRoomNotificationObserver]; + leaveRoomNotificationObserver = nil; + } } #pragma mark - -- (void)setRoomParticipants:(NSArray *)roomParticipants +- (void)setMxRoom:(MXRoom *)mxRoom { - participants = [NSMutableArray arrayWithArray:roomParticipants]; -} - -- (NSArray*)roomParticipants -{ - return participants; + // Remove the previous live listener + if (membersListener) + { + [self.mxRoom removeListener:membersListener]; + } + + _mxRoom = mxRoom; + + // Refresh displayed participants from the current room members + [self refreshParticipantsFromRoomMembers]; + + if (mxRoom) + { + // Register a listener for events that concern room members + NSArray *mxMembersEvents = @[kMXEventTypeStringRoomMember, kMXEventTypeStringRoomPowerLevels]; + membersListener = [self.mxRoom listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) { + + // Consider only live event + if (direction == MXEventDirectionForwards) + { + switch (event.eventType) + { + case MXEventTypeRoomMember: + { + // Take into account updated member + // Ignore here change related to the current user (this change is handled by leaveRoomNotificationObserver) + if ([event.userId isEqualToString:self.mxRoom.mxSession.myUser.userId] == NO) + { + MXRoomMember *mxMember = [self.mxRoom.state memberWithUserId:event.userId]; + if (mxMember) + { + [self addRoomMemberToParticipants:mxMember]; + } + } + + if ([event.stateKey isEqualToString:self.mxRoom.mxSession.myUser.userId] == NO) + { + MXRoomMember *mxMember = [self.mxRoom.state memberWithUserId:event.stateKey]; + if (mxMember) + { + [self addRoomMemberToParticipants:mxMember]; + } + } + + break; + } + case MXEventTypeRoomPowerLevels: + { + [self refreshParticipantsFromRoomMembers]; + break; + } + default: + break; + + } + + // Refresh participants display (if visible) + if (participantsSection != -1) + { + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (participantsSection, 1)]; + [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + } + } + + }]; + } + + [self.tableView reloadData]; } - (void)setIsAddParticipantSearchBarEditing:(BOOL)isAddParticipantsSearchBarEditing @@ -121,6 +245,154 @@ } } +#pragma mark - Internals + +- (void)refreshParticipantsFromRoomMembers +{ + // Flush existing participants list + mutableParticipants = [NSMutableArray array]; + mxkContactsById = [NSMutableDictionary dictionary]; + userMatrixId = nil; + + if (self.mxRoom) + { + NSArray *members = self.mxRoom.state.members; + NSString *userId = self.mxRoom.mxSession.myUser.userId; + + for (MXRoomMember *mxMember in members) + { + // Update the current participants list + if ([mxMember.userId isEqualToString:userId]) + { + if (mxMember.membership == MXMembershipJoin || mxMember.membership == MXMembershipInvite) + { + // The user is in this room + userMatrixId = userId; + } + } + else + { + [self addRoomMemberToParticipants:mxMember]; + } + } + } +} + +- (void)addRoomMemberToParticipants:(MXRoomMember*)mxMember +{ + // Remove previous occurrence of this member (if any) + if (mutableParticipants.count) + { + NSUInteger index = [mutableParticipants indexOfObject:mxMember.userId]; + if (index != NSNotFound) + { + [mxkContactsById removeObjectForKey:mxMember.userId]; + [mutableParticipants removeObjectAtIndex:index]; + } + } + + // Add this member after checking his status + if (mxMember.membership == MXMembershipJoin || mxMember.membership == MXMembershipInvite) + { + // Check whether this member is admin + BOOL isAdmin = ([self.mxRoom.state memberNormalizedPowerLevel:mxMember.userId] == 1); + + // Prepare the display name of this member + NSString *displayName = mxMember.displayname; + if (displayName.length == 0) + { + // Look for the corresponding MXUser in matrix session + MXUser *mxUser = [self.mxRoom.mxSession userWithUserId:mxMember.userId]; + if (mxUser) + { + displayName = ((mxUser.displayname.length > 0) ? mxUser.displayname : mxMember.userId); + } + else + { + displayName = mxMember.userId; + } + } + if (isAdmin) + { + displayName = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_admin_name", @"Vector", nil), displayName]; + } + + // Create the contact related to this member + MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:mxMember.userId]; + [mxkContactsById setObject:contact forKey:mxMember.userId]; + + // Add this participant (admin is in first position, the other are sorted in alphabetical order). + NSUInteger index = 0; + if (isAdmin) + { + // Check whether there is other admin + for (NSString *userId in mutableParticipants) + { + if ([self.mxRoom.state memberNormalizedPowerLevel:userId] == 1) + { + contact = [mxkContactsById objectForKey:userId]; + + // Sort admin in alphabetical order + if ([displayName compare:contact.displayName options:NSCaseInsensitiveSearch] != NSOrderedDescending) + { + break; + } + index++; + } + } + } + else + { + for (NSString *userId in mutableParticipants) + { + // Pass admin(s) + if ([self.mxRoom.state memberNormalizedPowerLevel:userId] == 1) + { + index++; + } + else + { + contact = [mxkContactsById objectForKey:userId]; + + // Sort in alphabetical order + if ([displayName compare:contact.displayName options:NSCaseInsensitiveSearch] != NSOrderedDescending) + { + break; + } + index++; + } + } + } + + // Add this participant + [mutableParticipants insertObject:mxMember.userId atIndex:index]; + } +} + +- (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; + } +} + #pragma mark - UITableView data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView @@ -157,8 +429,8 @@ } else if (section == participantsSection) { - count = self.roomParticipants.count; - if (self.userMatrixId) + count = mutableParticipants.count; + if (userMatrixId) { count++; } @@ -175,8 +447,8 @@ } else if (section == participantsSection) { - NSInteger count = self.roomParticipants.count; - if (self.userMatrixId) + NSInteger count = mutableParticipants.count; + if (userMatrixId) { count++; } @@ -236,8 +508,11 @@ // Show 'add' icon. filteredParticipantCell.contactAccessoryViewType = MXKContactTableCellAccessoryCustom; filteredParticipantCell.contactAccessoryViewHeightConstraint.constant = 30; - filteredParticipantCell.contactAccessoryView.image = [UIImage imageNamed:@"add"]; + filteredParticipantCell.contactAccessoryViewWidthConstraint.constant = 30; + filteredParticipantCell.contactAccessoryImageView.image = [UIImage imageNamed:@"add"]; + filteredParticipantCell.contactAccessoryImageView.hidden = NO; filteredParticipantCell.contactAccessoryView.hidden = NO; + filteredParticipantCell.selectionStyle = UITableViewCellSelectionStyleNone; cell = filteredParticipantCell; @@ -251,38 +526,43 @@ participantCell = [[MXKContactTableCell alloc] init]; } - if (self.userMatrixId && indexPath.row == 0) + if (userMatrixId && indexPath.row == 0) { - MXKContact *contact = [participantsByIds objectForKey:self.userMatrixId]; + MXKContact *contact = [mxkContactsById objectForKey:userMatrixId]; if (! contact) { - contact = [[MXKContact alloc] initMatrixContactWithDisplayName:NSLocalizedStringFromTable(@"you", @"Vector", nil) andMatrixID:self.userMatrixId]; - if (!participantsByIds) - { - participantsByIds = [NSMutableDictionary dictionary]; - } - [participantsByIds setObject:contact forKey:self.userMatrixId]; + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:NSLocalizedStringFromTable(@"you", @"Vector", nil) andMatrixID:userMatrixId]; + [mxkContactsById setObject:contact forKey:userMatrixId]; } [participantCell render:contact]; - // Hide accessory view. + // Show 'Leave' buton. participantCell.contactAccessoryViewType = MXKContactTableCellAccessoryCustom; + UIButton *actionButton = participantCell.contactAccessoryButton; + actionButton.hidden = NO; + [actionButton setTitle:NSLocalizedStringFromTable(@"leave", @"Vector", nil) forState:UIControlStateNormal]; + [actionButton sizeToFit]; + participantCell.contactAccessoryViewHeightConstraint.constant = actionButton.frame.size.height; + participantCell.contactAccessoryViewWidthConstraint.constant = actionButton.frame.size.width; + [participantCell needsUpdateConstraints]; + participantCell.contactAccessoryView.hidden = NO; + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; } else { NSInteger index = indexPath.row; - if (self.userMatrixId) + if (userMatrixId) { index --; } - if (index < participants.count) + if (index < mutableParticipants.count) { - NSString *userId = participants[index]; - MXKContact *contact = [participantsByIds objectForKey:userId]; + NSString *userId = mutableParticipants[index]; + MXKContact *contact = [mxkContactsById objectForKey:userId]; if (!contact) { // Create this missing contact @@ -301,11 +581,7 @@ if (contact) { - if (!participantsByIds) - { - participantsByIds = [NSMutableDictionary dictionary]; - } - [participantsByIds setObject:contact forKey:userId]; + [mxkContactsById setObject:contact forKey:userId]; } } @@ -315,11 +591,19 @@ [participantCell render:contact]; } - // Show 'remove' icon. + // Show 'remove' button. participantCell.contactAccessoryViewType = MXKContactTableCellAccessoryCustom; - participantCell.contactAccessoryViewHeightConstraint.constant = 30; - participantCell.contactAccessoryView.image = [UIImage imageNamed:@"remove"]; + UIButton *actionButton = participantCell.contactAccessoryButton; + actionButton.hidden = NO; + [actionButton setTitle:NSLocalizedStringFromTable(@"remove", @"Vector", nil) forState:UIControlStateNormal]; + [actionButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + actionButton.tag = indexPath.row; + [actionButton sizeToFit]; + + participantCell.contactAccessoryViewHeightConstraint.constant = actionButton.frame.size.height; + participantCell.contactAccessoryViewWidthConstraint.constant = actionButton.frame.size.width; participantCell.contactAccessoryView.hidden = NO; + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; } } @@ -352,11 +636,6 @@ } } -//- (CGFloat) tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section -//{ -// return 1; -//} - - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger index = indexPath.row; @@ -370,41 +649,162 @@ NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { - [participants addObject:identifiers.firstObject]; + NSString *participantId = identifiers.firstObject; // Handle a mapping contact by userId for selected participants - if (!participantsByIds) + [mxkContactsById setObject:contact forKey:participantId]; + + // Invite this user if a room is defined + if (self.mxRoom) { - participantsByIds = [NSMutableDictionary dictionary]; + [self addPendingActionMask]; + [self.mxRoom inviteUser:participantId success:^{ + + [self removePendingActionMask]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; + + } failure:^(NSError *error) { + + [self removePendingActionMask]; + + NSLog(@"[RoomParticipantsVC] Invite %@ failed: %@", participantId, error); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + } + else + { + // Update here the mutable list of participants + [mutableParticipants addObject:participantId]; + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; } - [participantsByIds setObject:contact forKey:identifiers.firstObject]; } -// [filteredParticipants removeObjectAtIndex:index]; -// // Refresh display -// [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; - // Leave search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; } } - else if (indexPath.section == participantsSection) - { - if (self.userMatrixId) +} + +#pragma mark - Actions + +- (IBAction)onButtonPressed:(id)sender +{ + if ([sender isKindOfClass:[UIButton class]]) + { + UIButton *actionButton = (UIButton*)sender; + NSInteger index = actionButton.tag; + __weak typeof(self) weakSelf = self; + + if (currentAlert) { - index --; + [currentAlert dismiss:NO]; + currentAlert = nil; } - if (index < participants.count) + if (userMatrixId && index == 0 && [actionButton.titleLabel.text isEqualToString:NSLocalizedStringFromTable(@"leave", @"Vector", nil)]) { - [participantsByIds removeObjectForKey:participants[index]]; - [participants removeObjectAtIndex:index]; + // Leave ? + currentAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"room_participants_leave_prompt_title", @"Vector", nil) + message:NSLocalizedStringFromTable(@"room_participants_leave_prompt_msg", @"Vector", nil) + style:MXKAlertStyleAlert]; - // Refresh display - [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:MXKAlertActionStyleCancel + handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + }]; - UITableViewHeaderFooterView *participantsSectionHeader = [self.tableView headerViewForSection:participantsSection]; - participantsSectionHeader.textLabel.text = [self tableView:self.tableView titleForHeaderInSection:participantsSection]; + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"leave", @"Vector", nil) + style:MXKAlertActionStyleDefault + handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + [strongSelf addPendingActionMask]; + [strongSelf.mxRoom leave:^{ + + [strongSelf withdrawViewControllerAnimated:YES completion:nil]; + + } failure:^(NSError *error) { + + [strongSelf removePendingActionMask]; + NSLog(@"[RoomParticipantsVC] Leave room %@ failed: %@", strongSelf.mxRoom.state.roomId, error); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + + }]; + + [currentAlert showInViewController:self]; + } + else if ([actionButton.titleLabel.text isEqualToString:NSLocalizedStringFromTable(@"remove", @"Vector", nil)]) + { + if (userMatrixId) + { + index --; + } + + if (index < mutableParticipants.count) + { + NSString *memberUserId = mutableParticipants[index]; + MXKContact *contact = [mxkContactsById objectForKey:memberUserId]; + + // Kick ? + NSString *promptMsg = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_remove_prompt_msg", @"Vector", nil), (contact ? contact.displayName : memberUserId)]; + currentAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"room_participants_remove_prompt_title", @"Vector", nil) + message:promptMsg + style:MXKAlertStyleAlert]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:MXKAlertActionStyleCancel + handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + }]; + + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"remove", @"Vector", nil) + style:MXKAlertActionStyleDefault + handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + [strongSelf addPendingActionMask]; + [strongSelf.mxRoom kickUser:memberUserId + reason:nil + success:^{ + + [strongSelf removePendingActionMask]; + + [strongSelf->mxkContactsById removeObjectForKey:memberUserId]; + [strongSelf->mutableParticipants removeObjectAtIndex:index]; + + // Refresh display + [strongSelf.tableView reloadData]; + + } failure:^(NSError *error) { + + [strongSelf removePendingActionMask]; + NSLog(@"[RoomParticipantsVC] Kick %@ failed: %@", memberUserId, error); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + }]; + + [currentAlert showInViewController:self]; + } } } } @@ -434,9 +834,9 @@ { for (NSString *userId in identifiers) { - if (!participants || [participants indexOfObject:userId] == NSNotFound) + if (!mutableParticipants || [mutableParticipants indexOfObject:userId] == NSNotFound) { - if (![userId isEqualToString:self.userMatrixId]) + if (![userId isEqualToString:userMatrixId]) { MXKContact *splitContact = [[MXKContact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; [mxUsers addObject:splitContact]; @@ -447,9 +847,9 @@ else if (identifiers.count) { NSString *userId = identifiers.firstObject; - if (!participants || [participants indexOfObject:userId] == NSNotFound) + if (!mutableParticipants || [mutableParticipants indexOfObject:userId] == NSNotFound) { - if (![userId isEqualToString:self.userMatrixId]) + if (![userId isEqualToString:userMatrixId]) { [mxUsers addObject:contact]; } diff --git a/Vector/ViewController/RoomViewController.m b/Vector/ViewController/RoomViewController.m index 5efdf59dd..a9b5de413 100644 --- a/Vector/ViewController/RoomViewController.m +++ b/Vector/ViewController/RoomViewController.m @@ -21,6 +21,8 @@ #import "RoomInputToolbarView.h" +#import "RoomParticipantsViewController.h" + @interface RoomViewController () { // The constraint used to animate menu list display @@ -295,34 +297,15 @@ id pushedViewController = [segue destinationViewController]; - if ([[segue identifier] isEqualToString:@"showMemberList"]) + if ([[segue identifier] isEqualToString:@"showRoomParticipants"]) { - if ([pushedViewController isKindOfClass:[MXKRoomMemberListViewController class]]) + if ([pushedViewController isKindOfClass:[RoomParticipantsViewController class]]) { - MXKRoomMemberListViewController* membersController = (MXKRoomMemberListViewController*)pushedViewController; - // Dismiss keyboard [self dismissKeyboard]; - MXKRoomMemberListDataSource *membersDataSource = [[MXKRoomMemberListDataSource alloc] initWithRoomId:self.roomDataSource.roomId andMatrixSession:self.mainSession]; - [membersDataSource finalizeInitialization]; - - [membersController displayList:membersDataSource]; - } - } - else if ([[segue identifier] isEqualToString:@"showMemberDetails"]) - { - if (selectedRoomMember) - { - MXKRoomMemberDetailsViewController *memberViewController = pushedViewController; - // Set rageShake handler - memberViewController.rageShakeManager = [RageShakeManager sharedManager]; - // Set delegate to handle start chat option - memberViewController.delegate = [AppDelegate theDelegate]; - - [memberViewController displayRoomMember:selectedRoomMember withMatrixRoom:self.roomDataSource.room]; - - selectedRoomMember = nil; + RoomParticipantsViewController* participantsViewController = (RoomParticipantsViewController*)pushedViewController; + participantsViewController.mxRoom = self.roomDataSource.room; } } } @@ -384,7 +367,7 @@ } else if (sender == self.participantsMenuButton) { - [self performSegueWithIdentifier:@"showMemberList" sender:self]; + [self performSegueWithIdentifier:@"showRoomParticipants" sender:self]; } else if (sender == self.favouriteMenuButton) {