AddParticipantsTableViewController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. /**
  2. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. #import "AddParticipantsTableViewController.h"
  6. #import "NCAPIController.h"
  7. #import "NCAppBranding.h"
  8. #import "NCContact.h"
  9. #import "NCDatabaseManager.h"
  10. #import "NCUserInterfaceController.h"
  11. #import "PlaceholderView.h"
  12. #import "ResultMultiSelectionTableViewController.h"
  13. #import "NextcloudTalk-Swift.h"
  14. @interface AddParticipantsTableViewController () <UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating>
  15. {
  16. NSMutableDictionary *_participants;
  17. NSArray *_indexes;
  18. NCRoom *_room;
  19. NSArray *_participantsInRoom;
  20. UISearchController *_searchController;
  21. ResultMultiSelectionTableViewController *_resultTableViewController;
  22. NSMutableArray *_selectedParticipants;
  23. PlaceholderView *_participantsBackgroundView;
  24. NSTimer *_searchTimer;
  25. NSURLSessionTask *_searchParticipantsTask;
  26. UIActivityIndicatorView *_addingParticipantsIndicator;
  27. BOOL _errorAddingParticipants;
  28. }
  29. @end
  30. @implementation AddParticipantsTableViewController
  31. - (instancetype)initForRoom:(NCRoom *)room
  32. {
  33. self = [super init];
  34. if (!self) {
  35. return nil;
  36. }
  37. if (room) {
  38. _room = room;
  39. _participantsInRoom = [room.participants valueForKey:@"self"];
  40. }
  41. _participants = [[NSMutableDictionary alloc] init];
  42. _indexes = [[NSArray alloc] init];
  43. _selectedParticipants = [[NSMutableArray alloc] init];
  44. _addingParticipantsIndicator = [[UIActivityIndicatorView alloc] init];
  45. _addingParticipantsIndicator.color = [NCAppBranding themeTextColor];
  46. return self;
  47. }
  48. - (instancetype)initWithParticipants:(NSArray<NCUser *> *)participants
  49. {
  50. self = [self initForRoom:nil];
  51. if (!self) {
  52. return nil;
  53. }
  54. _selectedParticipants = [[NSMutableArray alloc] initWithArray:participants];
  55. return self;
  56. }
  57. - (void)viewDidLoad
  58. {
  59. [super viewDidLoad];
  60. [self.tableView registerNib:[UINib nibWithNibName:kContactsTableCellNibName bundle:nil] forCellReuseIdentifier:kContactCellIdentifier];
  61. self.tableView.separatorInset = UIEdgeInsetsMake(0, 72, 0, 0);
  62. self.tableView.sectionIndexBackgroundColor = [UIColor clearColor];
  63. _resultTableViewController = [[ResultMultiSelectionTableViewController alloc] init];
  64. _resultTableViewController.selectedParticipants = _selectedParticipants;
  65. _resultTableViewController.room = _room;
  66. _searchController = [[UISearchController alloc] initWithSearchResultsController:_resultTableViewController];
  67. _searchController.searchResultsUpdater = self;
  68. [_searchController.searchBar sizeToFit];
  69. UIColor *themeColor = [NCAppBranding themeColor];
  70. UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
  71. [appearance configureWithOpaqueBackground];
  72. appearance.backgroundColor = themeColor;
  73. appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
  74. self.navigationItem.standardAppearance = appearance;
  75. self.navigationItem.compactAppearance = appearance;
  76. self.navigationItem.scrollEdgeAppearance = appearance;
  77. self.navigationItem.searchController = _searchController;
  78. self.navigationItem.searchController.searchBar.searchTextField.backgroundColor = [NCUtils searchbarBGColorForColor:themeColor];
  79. if (@available(iOS 16.0, *)) {
  80. self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
  81. }
  82. _searchController.searchBar.tintColor = [NCAppBranding themeTextColor];
  83. UITextField *searchTextField = [_searchController.searchBar valueForKey:@"searchField"];
  84. UIButton *clearButton = [searchTextField valueForKey:@"_clearButton"];
  85. searchTextField.tintColor = [NCAppBranding themeTextColor];
  86. searchTextField.textColor = [NCAppBranding themeTextColor];
  87. searchTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
  88. dispatch_async(dispatch_get_main_queue(), ^{
  89. // Search bar placeholder
  90. searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Search", nil)
  91. attributes:@{NSForegroundColorAttributeName:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]}];
  92. // Search bar search icon
  93. UIImageView *searchImageView = (UIImageView *)searchTextField.leftView;
  94. searchImageView.image = [searchImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  95. [searchImageView setTintColor:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]];
  96. // Search bar search clear button
  97. UIImage *clearButtonImage = [clearButton.imageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  98. [clearButton setImage:clearButtonImage forState:UIControlStateNormal];
  99. [clearButton setImage:clearButtonImage forState:UIControlStateHighlighted];
  100. [clearButton setTintColor:[NCAppBranding themeTextColor]];
  101. });
  102. self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
  103. // Contacts placeholder view
  104. _participantsBackgroundView = [[PlaceholderView alloc] init];
  105. [_participantsBackgroundView setImage:[UIImage imageNamed:@"contacts-placeholder"]];
  106. [_participantsBackgroundView.placeholderTextView setText:NSLocalizedString(@"No participants found", nil)];
  107. [_participantsBackgroundView.placeholderView setHidden:YES];
  108. [_participantsBackgroundView.loadingView startAnimating];
  109. self.tableView.backgroundView = _participantsBackgroundView;
  110. // We want ourselves to be the delegate for the result table so didSelectRowAtIndexPath is called for both tables.
  111. _resultTableViewController.tableView.delegate = self;
  112. _searchController.delegate = self;
  113. _searchController.searchBar.delegate = self;
  114. _searchController.hidesNavigationBarDuringPresentation = NO;
  115. [self updateCounter];
  116. self.definesPresentationContext = YES;
  117. UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
  118. target:self action:@selector(cancelButtonPressed)];
  119. self.navigationController.navigationBar.topItem.leftBarButtonItem = cancelButton;
  120. self.navigationItem.title = NSLocalizedString(@"Add participants", nil);
  121. [self.navigationController.navigationBar setTitleTextAttributes:
  122. @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]}];
  123. self.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
  124. self.navigationController.navigationBar.translucent = NO;
  125. self.navigationController.navigationBar.barTintColor = [NCAppBranding themeColor];
  126. // Fix uisearchcontroller animation
  127. self.extendedLayoutIncludesOpaqueBars = YES;
  128. }
  129. - (void)dealloc
  130. {
  131. [[NSNotificationCenter defaultCenter] removeObserver:self];
  132. }
  133. - (void)viewDidAppear:(BOOL)animated
  134. {
  135. [super viewDidAppear:animated];
  136. self.navigationItem.hidesSearchBarWhenScrolling = NO;
  137. [self getPossibleParticipants];
  138. }
  139. - (void)viewWillAppear:(BOOL)animated
  140. {
  141. [super viewWillAppear:animated];
  142. self.navigationItem.hidesSearchBarWhenScrolling = NO;
  143. }
  144. - (void)didReceiveMemoryWarning
  145. {
  146. [super didReceiveMemoryWarning];
  147. }
  148. #pragma mark - View controller actions
  149. - (void)cancelButtonPressed
  150. {
  151. [self close];
  152. }
  153. - (void)close
  154. {
  155. if ([self.delegate respondsToSelector:@selector(addParticipantsTableViewControllerDidFinish:)]) {
  156. [self.delegate addParticipantsTableViewControllerDidFinish:self];
  157. }
  158. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  159. }
  160. - (void)addButtonPressed
  161. {
  162. if (_room && _selectedParticipants.count > 0) {
  163. dispatch_group_t addParticipantsGroup = dispatch_group_create();
  164. [self showAddingParticipantsView];
  165. for (NCUser *participant in _selectedParticipants) {
  166. [self addParticipantToRoom:participant withDispatchGroup:addParticipantsGroup];
  167. }
  168. dispatch_group_notify(addParticipantsGroup, dispatch_get_main_queue(), ^{
  169. [self removeAddingParticipantsView];
  170. if (!self->_errorAddingParticipants) {
  171. [self close];
  172. }
  173. // Reset flag once adding participants process has finished
  174. self->_errorAddingParticipants = NO;
  175. });
  176. } else if ([self.delegate respondsToSelector:@selector(addParticipantsTableViewController:wantsToAdd:)]) {
  177. [self.delegate addParticipantsTableViewController:self wantsToAdd:_selectedParticipants];
  178. [self close];
  179. }
  180. }
  181. - (void)addParticipantToRoom:(NCUser *)participant withDispatchGroup:(dispatch_group_t)dispatchGroup
  182. {
  183. dispatch_group_enter(dispatchGroup);
  184. [[NCAPIController sharedInstance] addParticipant:participant.userId ofType:participant.source toRoom:_room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) {
  185. if (error) {
  186. UIAlertController * alert = [UIAlertController
  187. alertControllerWithTitle:NSLocalizedString(@"Could not add participant", nil)
  188. message:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while adding %@ to the room", nil), participant.name]
  189. preferredStyle:UIAlertControllerStyleAlert];
  190. UIAlertAction* okButton = [UIAlertAction
  191. actionWithTitle:NSLocalizedString(@"OK", nil)
  192. style:UIAlertActionStyleDefault
  193. handler:nil];
  194. [alert addAction:okButton];
  195. self->_errorAddingParticipants = YES;
  196. [[NCUserInterfaceController sharedInstance] presentAlertViewController:alert];
  197. }
  198. dispatch_group_leave(dispatchGroup);
  199. }];
  200. }
  201. - (void)updateCounter
  202. {
  203. UIBarButtonItem *addButton = nil;
  204. if (!_room) {
  205. addButton = [[UIBarButtonItem alloc]
  206. initWithBarButtonSystemItem:UIBarButtonSystemItemDone
  207. target:self
  208. action:@selector(addButtonPressed)];
  209. } else if (_selectedParticipants.count > 0) {
  210. addButton = [[UIBarButtonItem alloc]
  211. initWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Add (%lu)", nil), (unsigned long)_selectedParticipants.count]
  212. style:UIBarButtonItemStylePlain
  213. target:self
  214. action:@selector(addButtonPressed)];
  215. }
  216. self.navigationController.navigationBar.topItem.rightBarButtonItem = addButton;
  217. }
  218. - (void)showAddingParticipantsView
  219. {
  220. [_addingParticipantsIndicator startAnimating];
  221. UIBarButtonItem *addingParticipantButton = [[UIBarButtonItem alloc] initWithCustomView:_addingParticipantsIndicator];
  222. self.navigationItem.rightBarButtonItems = @[addingParticipantButton];
  223. self.tableView.allowsSelection = NO;
  224. _resultTableViewController.tableView.allowsSelection = NO;
  225. }
  226. - (void)removeAddingParticipantsView
  227. {
  228. [_addingParticipantsIndicator stopAnimating];
  229. [self updateCounter];
  230. self.tableView.allowsSelection = YES;
  231. _resultTableViewController.tableView.allowsSelection = YES;
  232. }
  233. #pragma mark - Participants actions
  234. - (NSMutableArray *)filterContacts:(NSMutableArray *)contacts
  235. {
  236. NSMutableArray *participants = [[NSMutableArray alloc] init];
  237. for (NCUser *user in contacts) {
  238. if (![_participantsInRoom containsObject:user.userId]) {
  239. [participants addObject:user];
  240. } else if (![user.source isEqualToString:kParticipantTypeUser]) {
  241. [participants addObject:user];
  242. }
  243. }
  244. return participants;
  245. }
  246. - (void)getPossibleParticipants
  247. {
  248. TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
  249. [[NCAPIController sharedInstance] getContactsForAccount:activeAccount forRoom:_room.token groupRoom:YES withSearchParam:nil andCompletionBlock:^(NSArray *indexes, NSMutableDictionary *contacts, NSMutableArray *contactList, NSError *error) {
  250. if (!error) {
  251. NSMutableArray *storedContacts = [NCContact contactsForAccountId:activeAccount.accountId contains:nil];
  252. NSMutableArray *combinedContactList = [NCUser combineUsersArray:storedContacts withUsersArray:contactList];
  253. NSMutableArray *filteredParticipants = [self filterContacts:combinedContactList];
  254. NSMutableDictionary *participants = [NCUser indexedUsersFromUsersArray:filteredParticipants];
  255. self->_participants = participants;
  256. self->_indexes = [[participants allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
  257. [self->_participantsBackgroundView.loadingView stopAnimating];
  258. [self->_participantsBackgroundView.loadingView setHidden:YES];
  259. [self->_participantsBackgroundView.placeholderView setHidden:(participants.count > 0)];
  260. [self.tableView reloadData];
  261. } else {
  262. NSLog(@"Error while trying to get participants: %@", error);
  263. }
  264. }];
  265. }
  266. - (void)searchForParticipantsWithString:(NSString *)searchString
  267. {
  268. [_searchParticipantsTask cancel];
  269. TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
  270. _searchParticipantsTask = [[NCAPIController sharedInstance] getContactsForAccount:[[NCDatabaseManager sharedInstance] activeAccount] forRoom:_room.token groupRoom:YES withSearchParam:searchString andCompletionBlock:^(NSArray *indexes, NSMutableDictionary *contacts, NSMutableArray *contactList, NSError *error) {
  271. if (!error) {
  272. NSMutableArray *storedContacts = [NCContact contactsForAccountId:activeAccount.accountId contains:searchString];
  273. NSMutableArray *combinedContactList = [NCUser combineUsersArray:storedContacts withUsersArray:contactList];
  274. NSMutableArray *filteredParticipants = [self filterContacts:combinedContactList];
  275. NSMutableDictionary *participants = [NCUser indexedUsersFromUsersArray:filteredParticipants];
  276. NSArray *sortedIndexes = [[participants allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
  277. [self->_resultTableViewController setSearchResultContacts:participants withIndexes:sortedIndexes];
  278. } else {
  279. if (error.code != -999) {
  280. NSLog(@"Error while searching for participants: %@", error);
  281. }
  282. }
  283. }];
  284. }
  285. - (BOOL)isParticipantAlreadySelected:(NCUser *)participant
  286. {
  287. for (NCUser *user in _selectedParticipants) {
  288. if ([user.userId isEqualToString:participant.userId] &&
  289. [user.source isEqualToString:participant.source]) {
  290. return YES;
  291. }
  292. }
  293. return NO;
  294. }
  295. - (void)removeSelectedParticipant:(NCUser *)participant
  296. {
  297. NCUser *userToDelete = nil;
  298. for (NCUser *user in _selectedParticipants) {
  299. if ([user.userId isEqualToString:participant.userId] &&
  300. [user.source isEqualToString:participant.source]) {
  301. userToDelete = user;
  302. }
  303. }
  304. if (userToDelete) {
  305. [_selectedParticipants removeObject:userToDelete];
  306. }
  307. }
  308. #pragma mark - Search controller
  309. - (void)updateSearchResultsForSearchController:(UISearchController *)searchController
  310. {
  311. [_searchTimer invalidate];
  312. _searchTimer = nil;
  313. [_resultTableViewController showSearchingUI];
  314. dispatch_async(dispatch_get_main_queue(), ^{
  315. self->_searchTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(searchForParticipants) userInfo:nil repeats:NO];
  316. });
  317. }
  318. - (void)searchForParticipants
  319. {
  320. NSString *searchString = _searchController.searchBar.text;
  321. if (![searchString isEqualToString:@""]) {
  322. [self searchForParticipantsWithString:searchString];
  323. }
  324. }
  325. - (void)didDismissSearchController:(UISearchController *)searchController
  326. {
  327. [self.tableView reloadData];
  328. }
  329. #pragma mark - Table view data source
  330. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  331. return _indexes.count;
  332. }
  333. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  334. NSString *index = [_indexes objectAtIndex:section];
  335. NSArray *participants = [_participants objectForKey:index];
  336. return participants.count;
  337. }
  338. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  339. {
  340. return kContactsTableCellHeight;
  341. }
  342. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  343. {
  344. return [_indexes objectAtIndex:section];
  345. }
  346. - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
  347. {
  348. return _indexes;
  349. }
  350. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  351. {
  352. NSString *index = [_indexes objectAtIndex:indexPath.section];
  353. NSArray *participants = [_participants objectForKey:index];
  354. NCUser *participant = [participants objectAtIndex:indexPath.row];
  355. ContactsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kContactCellIdentifier forIndexPath:indexPath];
  356. if (!cell) {
  357. cell = [[ContactsTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kContactCellIdentifier];
  358. }
  359. cell.labelTitle.text = participant.name;
  360. [cell.contactImage setActorAvatarForId:participant.userId withType:participant.source withDisplayName:participant.name withRoomToken:_room.token];
  361. UIImage *selectionImage = [UIImage systemImageNamed:@"circle"];
  362. UIColor *selectionImageColor = [UIColor tertiaryLabelColor];
  363. if ([self isParticipantAlreadySelected:participant]) {
  364. selectionImage = [UIImage systemImageNamed:@"checkmark.circle.fill"];
  365. selectionImageColor = [NCAppBranding elementColor];
  366. }
  367. UIImageView *selectionImageView = [[UIImageView alloc] initWithImage:selectionImage];
  368. selectionImageView.tintColor = selectionImageColor;
  369. cell.accessoryView = selectionImageView;
  370. return cell;
  371. }
  372. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  373. {
  374. NSString *index = nil;
  375. NSArray *participants = nil;
  376. if (_searchController.active && _resultTableViewController.contacts.count > 0) {
  377. index = [_resultTableViewController.indexes objectAtIndex:indexPath.section];
  378. participants = [_resultTableViewController.contacts objectForKey:index];
  379. } else {
  380. index = [_indexes objectAtIndex:indexPath.section];
  381. participants = [_participants objectForKey:index];
  382. }
  383. NCUser *participant = [participants objectAtIndex:indexPath.row];
  384. if (![self isParticipantAlreadySelected:participant]) {
  385. [_selectedParticipants addObject:participant];
  386. } else {
  387. [self removeSelectedParticipant:participant];
  388. }
  389. _resultTableViewController.selectedParticipants = _selectedParticipants;
  390. [tableView beginUpdates];
  391. [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
  392. [tableView endUpdates];
  393. [self updateCounter];
  394. }
  395. @end