diff --git a/Vector.xcodeproj/project.pbxproj b/Vector.xcodeproj/project.pbxproj index 48cc155b5..f6dbb7be0 100644 --- a/Vector.xcodeproj/project.pbxproj +++ b/Vector.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ F02D87C69D1FFCD2C1531F3D /* libPods-Vector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B179239B79688A61A3F465F /* libPods-Vector.a */; }; + F0316E3A1B7DF45A00F03620 /* add.png in Resources */ = {isa = PBXBuildFile; fileRef = F0316E381B7DF45A00F03620 /* add.png */; }; + F0316E3B1B7DF45A00F03620 /* add@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0316E391B7DF45A00F03620 /* add@2x.png */; }; + F0316E3E1B7E28DE00F03620 /* remove.png in Resources */ = {isa = PBXBuildFile; fileRef = F0316E3C1B7E28DE00F03620 /* remove.png */; }; + F0316E3F1B7E28DE00F03620 /* remove@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0316E3D1B7E28DE00F03620 /* remove@2x.png */; }; F094A9A81B78D8F000B1FBBF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F094A9A71B78D8F000B1FBBF /* main.m */; }; F094A9AB1B78D8F000B1FBBF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F094A9AA1B78D8F000B1FBBF /* AppDelegate.m */; }; F094A9B41B78D8F000B1FBBF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F094A9B21B78D8F000B1FBBF /* Main.storyboard */; }; @@ -69,6 +73,10 @@ 11865E69C29698A4179E1F3F /* Pods-Vector.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Vector.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Vector/Pods-Vector.debug.xcconfig"; sourceTree = ""; }; 435C7E1A9BC3DE28D526540F /* Pods-Vector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Vector.release.xcconfig"; path = "Pods/Target Support Files/Pods-Vector/Pods-Vector.release.xcconfig"; sourceTree = ""; }; 9B179239B79688A61A3F465F /* libPods-Vector.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Vector.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F0316E381B7DF45A00F03620 /* add.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = add.png; sourceTree = ""; }; + F0316E391B7DF45A00F03620 /* add@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "add@2x.png"; sourceTree = ""; }; + F0316E3C1B7E28DE00F03620 /* remove.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = remove.png; sourceTree = ""; }; + F0316E3D1B7E28DE00F03620 /* remove@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "remove@2x.png"; sourceTree = ""; }; F094A9A21B78D8F000B1FBBF /* Vector.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vector.app; sourceTree = BUILT_PRODUCTS_DIR; }; F094A9A61B78D8F000B1FBBF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F094A9A71B78D8F000B1FBBF /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -301,6 +309,10 @@ F0DD7D1B1B7AA8C900C4BE02 /* Images */ = { isa = PBXGroup; children = ( + F0316E3C1B7E28DE00F03620 /* remove.png */, + F0316E3D1B7E28DE00F03620 /* remove@2x.png */, + F0316E381B7DF45A00F03620 /* add.png */, + F0316E391B7DF45A00F03620 /* add@2x.png */, F0DD7D7C1B7AAA3C00C4BE02 /* icon_users.png */, F0DD7D7D1B7AAA3C00C4BE02 /* icon_users@2x.png */, F0DD7D1C1B7AA8C900C4BE02 /* attach.png */, @@ -412,6 +424,7 @@ F0DD7D651B7AA8C900C4BE02 /* logo@2x.png in Resources */, F0DD7D611B7AA8C900C4BE02 /* hide@2x.png in Resources */, F0DD7D751B7AA8C900C4BE02 /* typing@2x.png in Resources */, + F0316E3A1B7DF45A00F03620 /* add.png in Resources */, F0DD7D671B7AA8C900C4BE02 /* members@2x.png in Resources */, F0DD7D721B7AA8C900C4BE02 /* tab_settings.png in Resources */, F0DD7D7B1B7AA8C900C4BE02 /* voip@2x.png in Resources */, @@ -419,9 +432,12 @@ F0DD7D7E1B7AAA3C00C4BE02 /* icon_users.png in Resources */, F094AA2B1B78E42600B1FBBF /* Localizable.strings in Resources */, F0DD7D4C1B7AA8C900C4BE02 /* attach.png in Resources */, + F0316E3E1B7E28DE00F03620 /* remove.png in Resources */, + F0316E3F1B7E28DE00F03620 /* remove@2x.png in Resources */, F0DD7D691B7AA8C900C4BE02 /* menu@2x.png in Resources */, F0DD7D731B7AA8C900C4BE02 /* tab_settings@2x.png in Resources */, F0DD7D641B7AA8C900C4BE02 /* logo.png in Resources */, + F0316E3B1B7DF45A00F03620 /* add@2x.png in Resources */, F0DD7D7F1B7AAA3C00C4BE02 /* icon_users@2x.png in Resources */, F094A9B41B78D8F000B1FBBF /* Main.storyboard in Resources */, F0DD7D601B7AA8C900C4BE02 /* hide.png in Resources */, diff --git a/Vector/Assets/Images/add.png b/Vector/Assets/Images/add.png new file mode 100644 index 000000000..5bc2f6a6f Binary files /dev/null and b/Vector/Assets/Images/add.png differ diff --git a/Vector/Assets/Images/add@2x.png b/Vector/Assets/Images/add@2x.png new file mode 100644 index 000000000..2d6107ac5 Binary files /dev/null and b/Vector/Assets/Images/add@2x.png differ diff --git a/Vector/Assets/Images/remove.png b/Vector/Assets/Images/remove.png new file mode 100644 index 000000000..93e8cc466 Binary files /dev/null and b/Vector/Assets/Images/remove.png differ diff --git a/Vector/Assets/Images/remove@2x.png b/Vector/Assets/Images/remove@2x.png new file mode 100644 index 000000000..4239229fc Binary files /dev/null and b/Vector/Assets/Images/remove@2x.png differ diff --git a/Vector/Assets/en.lproj/Vector.strings b/Vector/Assets/en.lproj/Vector.strings index 321251adf..941b85e8d 100644 --- a/Vector/Assets/en.lproj/Vector.strings +++ b/Vector/Assets/en.lproj/Vector.strings @@ -56,6 +56,7 @@ "notification_settings_global_notification_settings" = "Global Notification Settings"; // Others +"you" = "You"; "public_room_section_title" = "Public Rooms (at %@):"; "bug_report_prompt" = "The application has crashed last time. Would you like to submit a crash report?"; "rage_shake_prompt" = "You seem to be shaking the phone in frustration. Would you like to submit a bug report?"; \ No newline at end of file diff --git a/Vector/Base.lproj/Main.storyboard b/Vector/Base.lproj/Main.storyboard index 4433f2219..06ac86ce1 100644 --- a/Vector/Base.lproj/Main.storyboard +++ b/Vector/Base.lproj/Main.storyboard @@ -194,7 +194,7 @@ - + @@ -220,18 +220,10 @@ - + - - - - - - - - @@ -276,10 +268,10 @@ - + - + diff --git a/Vector/ViewController/RoomCreationStep1ViewController.m b/Vector/ViewController/RoomCreationStep1ViewController.m index b15b129ef..4f0264c75 100644 --- a/Vector/ViewController/RoomCreationStep1ViewController.m +++ b/Vector/ViewController/RoomCreationStep1ViewController.m @@ -70,6 +70,11 @@ { [self addMatrixSession:mxSession]; } + + // Add a little white space below the navigation bar + UIEdgeInsets contentInset = self.tableView.contentInset; + contentInset.top += 15; + self.tableView.contentInset = contentInset; } - (void)didReceiveMemoryWarning @@ -415,6 +420,17 @@ return 30; } +- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section +{ + if([view isKindOfClass:[UITableViewHeaderFooterView class]]) + { + UITableViewHeaderFooterView *tableViewHeaderFooterView = (UITableViewHeaderFooterView *) view; + tableViewHeaderFooterView.textLabel.text = [tableViewHeaderFooterView.textLabel.text capitalizedString]; + tableViewHeaderFooterView.textLabel.font = [UIFont boldSystemFontOfSize:17]; + tableViewHeaderFooterView.textLabel.textColor = [UIColor blackColor]; + } +} + //- (CGFloat) tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section //{ // return 1; diff --git a/Vector/ViewController/RoomCreationStep2ViewController.h b/Vector/ViewController/RoomCreationStep2ViewController.h index c935a9a6b..c0f35eb1b 100644 --- a/Vector/ViewController/RoomCreationStep2ViewController.h +++ b/Vector/ViewController/RoomCreationStep2ViewController.h @@ -16,7 +16,7 @@ #import -@interface RoomCreationStep2ViewController : MXKTableViewController +@interface RoomCreationStep2ViewController : MXKTableViewController @property (nonatomic) MXKRoomCreationInputs* roomCreationInputs; diff --git a/Vector/ViewController/RoomCreationStep2ViewController.m b/Vector/ViewController/RoomCreationStep2ViewController.m index bbdfbcd64..f253151b0 100644 --- a/Vector/ViewController/RoomCreationStep2ViewController.m +++ b/Vector/ViewController/RoomCreationStep2ViewController.m @@ -27,13 +27,15 @@ UIButton *createButton; MXHTTPOperation *roomCreationRequest; - // Add participant - NSInteger addParticipantSection; - UISearchBar *participantsSearchBar; + // Add participants + NSInteger addParticipantsSection; + UISearchBar *addParticipantsSearchBar; + NSString *addParticipantsSearchText; NSMutableArray *filteredParticipants; // Participants NSInteger participantsSection; + NSMutableDictionary *participantsByIds; } @end @@ -65,8 +67,9 @@ - (void)destroy { createButton = nil; - participantsSearchBar = nil; + addParticipantsSearchBar = nil; filteredParticipants = nil; + participantsByIds = nil; [super destroy]; } @@ -153,10 +156,10 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { NSInteger count = 0; - createButtonSection = addParticipantSection = participantsSection = -1; + createButtonSection = addParticipantsSection = participantsSection = -1; createButtonSection = count++; - addParticipantSection = count++; + addParticipantsSection = count++; participantsSection = count++; return count; @@ -169,13 +172,17 @@ { count = 1; } - else if (section == addParticipantSection) + else if (section == addParticipantsSection) { count = 1 + filteredParticipants.count; } else if (section == participantsSection) { count = _roomCreationInputs.roomParticipants.count; + if (_roomCreationInputs.mxSession) + { + count++; + } } return count; @@ -183,7 +190,7 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - if (section == addParticipantSection) + if (section == addParticipantsSection) { return NSLocalizedStringFromTable(@"room_creation_add_participants", @"Vector", nil); } @@ -216,14 +223,123 @@ cell = createButtonCell; } - else if (indexPath.section == addParticipantSection) + else if (indexPath.section == addParticipantsSection) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"roomPictureCell"]; - cell.textLabel.text = @"todo"; + if (indexPath.row == 0) + { + MXKTableViewCellWithSearchBar *addParticipantsSearchCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithSearchBar defaultReuseIdentifier]]; + if (!addParticipantsSearchCell) + { + addParticipantsSearchCell = [[MXKTableViewCellWithSearchBar alloc] init]; + } + + addParticipantsSearchBar = addParticipantsSearchCell.mxkSearchBar; + addParticipantsSearchBar.backgroundColor = [UIColor clearColor]; + addParticipantsSearchBar.text = addParticipantsSearchText; + addParticipantsSearchBar.returnKeyType = UIReturnKeyDone; + addParticipantsSearchBar.delegate = self; + if (addParticipantsSearchText.length) + { + [addParticipantsSearchBar becomeFirstResponder]; + } + + cell = addParticipantsSearchCell; + } + else + { + NSInteger index = indexPath.row - 1; + if (index < filteredParticipants.count) + { + MXKContactTableCell* filteredParticipantCell = [tableView dequeueReusableCellWithIdentifier:[MXKContactTableCell defaultReuseIdentifier]]; + if (!filteredParticipantCell) + { + filteredParticipantCell = [[MXKContactTableCell alloc] init]; + } + + [filteredParticipantCell render:filteredParticipants[index]]; + + // Trick, the 'add' icon is displayed here by using the matrixUserIconview which is visible for matrix contact only. + filteredParticipantCell.matrixUserIconViewHeightConstraint.constant = 30; + filteredParticipantCell.matrixUserIconView.image = [UIImage imageNamed:@"add"]; + filteredParticipantCell.selectionStyle = UITableViewCellSelectionStyleDefault; + + cell = filteredParticipantCell; + } + } } else if (indexPath.section == participantsSection) { - //TODO + MXKContactTableCell *participantCell = [tableView dequeueReusableCellWithIdentifier:[MXKContactTableCell defaultReuseIdentifier]]; + if (!participantCell) + { + participantCell = [[MXKContactTableCell alloc] init]; + } + + if (_roomCreationInputs.mxSession && indexPath.row == 0) + { + MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:NSLocalizedStringFromTable(@"you", @"Vector", nil) andMatrixID:_roomCreationInputs.mxSession.myUser.userId]; + + [participantCell render:contact]; + + // Trick, remove matrixUserIconview image to hide it. + participantCell.matrixUserIconView.image = nil; + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + else + { + NSInteger index = indexPath.row; + NSArray *participants = _roomCreationInputs.roomParticipants; + + if (_roomCreationInputs.mxSession) + { + index --; + } + + if (index < participants.count) + { + NSString *userId = participants[index]; + MXKContact *contact = [participantsByIds objectForKey:userId]; + // Note: contact may be nil here if the participant has not been added into _roomCreationInputs by self. + if (!contact) + { + // Create this missing contact + // Look for the correpsonding 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) + { + if (!participantsByIds) + { + participantsByIds = [NSMutableDictionary dictionary]; + } + [participantsByIds setObject:contact forKey:userId]; + } + + } + + if (contact) + { + [participantCell render:contact]; + } + + // Trick, the 'remove' icon is displayed here by using the matrixUserIconview which is visible for matrix contact only. + participantCell.matrixUserIconViewHeightConstraint.constant = 30; + participantCell.matrixUserIconView.image = [UIImage imageNamed:@"remove"]; + participantCell.selectionStyle = UITableViewCellSelectionStyleDefault; + } + } + + cell = participantCell; } return cell; @@ -246,6 +362,17 @@ return 30; } +- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section +{ + if([view isKindOfClass:[UITableViewHeaderFooterView class]]) + { + UITableViewHeaderFooterView *tableViewHeaderFooterView = (UITableViewHeaderFooterView *) view; + tableViewHeaderFooterView.textLabel.text = [tableViewHeaderFooterView.textLabel.text capitalizedString]; + tableViewHeaderFooterView.textLabel.font = [UIFont boldSystemFontOfSize:17]; + tableViewHeaderFooterView.textLabel.textColor = [UIColor blackColor]; + } +} + //- (CGFloat) tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section //{ // return 1; @@ -253,9 +380,162 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - // TODO + if (indexPath.section == addParticipantsSection && indexPath.row) + { + NSInteger index = indexPath.row - 1; + if (index < filteredParticipants.count) + { + MXKContact *contact = filteredParticipants[index]; + + NSArray *identifiers = contact.matrixIdentifiers; + if (identifiers.count) + { + [_roomCreationInputs addParticipant:identifiers.firstObject]; + + // Handle a mapping contact by userId for selected participants + if (!participantsByIds) + { + participantsByIds = [NSMutableDictionary dictionary]; + } + [participantsByIds setObject:contact forKey:identifiers.firstObject]; + } + + [filteredParticipants removeObjectAtIndex:index]; + + // Refresh display + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (addParticipantsSection, 2)]; + [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + } + } + else if (indexPath.section == participantsSection) + { + NSInteger index = indexPath.row; + + if (_roomCreationInputs.mxSession) + { + index --; + } + + NSArray *participants = _roomCreationInputs.roomParticipants; + if (index < participants.count) + { + NSInteger firstSectionToRefresh = participantsSection; + + [_roomCreationInputs removeParticipant:participants[index]]; + + // Check whether this removed participant must be added to the search result if any + MXKContact *contact = [participantsByIds objectForKey:participants[index]]; + if (contact && addParticipantsSearchText.length && [contact matchedWithPatterns:@[addParticipantsSearchText]]) + { + [filteredParticipants addObject:contact]; + firstSectionToRefresh = addParticipantsSection; + } + [participantsByIds removeObjectForKey:participants[index]]; + + // Refresh display + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (firstSectionToRefresh, 2)]; + [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + } + } [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +#pragma mark - UISearchBar delegate + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText +{ + NSInteger previousFilteredCount = filteredParticipants.count; + NSMutableArray *mxUsers; + if (addParticipantsSearchText.length && [searchText hasPrefix:addParticipantsSearchText]) + { + mxUsers = filteredParticipants; + } + else + { + // Retrieve all known matrix users + NSArray *matrixContacts = [NSMutableArray arrayWithArray:[MXKContactManager sharedManager].matrixContacts]; + mxUsers = [NSMutableArray arrayWithCapacity:matrixContacts.count]; + + // Split contacts with several ids, and remove the current participants. + NSArray *participants = _roomCreationInputs.roomParticipants; + for (MXKContact* contact in matrixContacts) + { + NSArray *identifiers = contact.matrixIdentifiers; + if (identifiers.count > 1) + { + for (NSString *userId in identifiers) + { + if (!participants || [participants indexOfObject:userId] == NSNotFound) + { + if (![userId isEqualToString:_roomCreationInputs.mxSession.myUser.userId]) + { + MXKContact *splitContact = [[MXKContact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; + [mxUsers addObject:splitContact]; + } + } + } + } + else if (identifiers.count) + { + NSString *userId = identifiers.firstObject; + if (!participants || [participants indexOfObject:userId] == NSNotFound) + { + if (![userId isEqualToString:_roomCreationInputs.mxSession.myUser.userId]) + { + [mxUsers addObject:contact]; + } + } + } + } + } + addParticipantsSearchText = searchText; + + filteredParticipants = [NSMutableArray array]; + + for (MXKContact* contact in mxUsers) + { + if ([contact matchedWithPatterns:@[addParticipantsSearchText]]) + { + [filteredParticipants addObject:contact]; + } + } + + if (previousFilteredCount || filteredParticipants.count) + { + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (addParticipantsSection, 1)]; + [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + } +} + +- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar +{ + searchBar.showsCancelButton = YES; + return YES; +} + +- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar +{ + searchBar.showsCancelButton = NO; + return YES; +} + +- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar +{ + // "Done" key has been pressed + [searchBar resignFirstResponder]; +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar +{ + // Leave search + [searchBar resignFirstResponder]; + addParticipantsSearchBar = nil; + addParticipantsSearchText = nil; + filteredParticipants = nil; + + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (addParticipantsSection, 1)]; + [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; } @end