/* Copyright 2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd Copyright 2015 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "HomeViewController.h" #import "GeneratedInterface-Swift.h" #import "RecentsDataSource.h" #import "TableViewCellWithCollectionView.h" #import "RoomCollectionViewCell.h" #import "MXRoom+Riot.h" @interface HomeViewController () { RecentsDataSource *recentsDataSource; // Room edition NSInteger selectedSection; NSString *selectedRoomId; UISwipeGestureRecognizer *horizontalSwipeGestureRecognizer; UISwipeGestureRecognizer *verticalSwipeGestureRecognizer; // The content offset of the collection in which the edited room is displayed. // We store this value to prevent the collection view from scrolling to the beginning (observed on iOS < 10). CGFloat selectedCollectionViewContentOffset; } @property (nonatomic, strong) SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter; @property (nonatomic, strong) SecureBackupBannerCell *secureBackupBannerPrototypeCell; @property (nonatomic, strong) CrossSigningSetupBannerCell *keyVerificationSetupBannerPrototypeCell; @property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; @property (nonatomic, assign, readwrite) BOOL roomListDataReady; @property (nonatomic, strong) MXThrottler *collectionViewPaginationThrottler; @property(nonatomic) SpaceMembersCoordinatorBridgePresenter *spaceMembersCoordinatorBridgePresenter; @end @implementation HomeViewController + (instancetype)instantiate { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; HomeViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"HomeViewController"]; return viewController; } - (void)finalizeInit { [super finalizeInit]; selectedSection = -1; selectedRoomId = nil; selectedCollectionViewContentOffset = -1; self.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenHome]; self.collectionViewPaginationThrottler = [[MXThrottler alloc] initWithMinimumDelay:0.1]; } - (void)viewDidLoad { [super viewDidLoad]; if (!BuildSettings.newAppLayoutEnabled) { [self.tabBarController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; } self.roomListDataReady = NO; self.view.accessibilityIdentifier = @"HomeVCView"; self.recentsTableView.accessibilityIdentifier = @"HomeVCTableView"; // Tag the recents table with the its recents data source mode. // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). self.recentsTableView.tag = self.recentsDataSourceMode; self.recentsTableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; // Add the (+) button programmatically [self addFabButton]; // Register table view cells used for rooms collection. [self registerCellsWithCollectionViews]; // Change the table data source. It must be the home view controller itself. self.recentsTableView.dataSource = self; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (!BuildSettings.newAppLayoutEnabled) { [ThemeService.shared.theme applyStyleOnNavigationBar:[AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar]; [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; } if (recentsDataSource.recentsDataSourceMode != self.recentsDataSourceMode) { // Take the lead on the shared data source. [recentsDataSource setDelegate:self andRecentsDataSourceMode:self.recentsDataSourceMode]; // Reset filtering on the shared data source when switching tabs [recentsDataSource searchWithPatterns:nil]; [self.recentsSearchBar setText:nil]; } } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator { if (selectedRoomId) { // Cancel room edition in case of device screen rotation. [self cancelEditionMode:YES]; } [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; } - (void)destroy { [super destroy]; } - (SecureBackupBannerCell *)secureBackupBannerPrototypeCell { if (!_secureBackupBannerPrototypeCell) { _secureBackupBannerPrototypeCell = [self.recentsTableView dequeueReusableCellWithIdentifier:SecureBackupBannerCell.defaultReuseIdentifier]; } return _secureBackupBannerPrototypeCell; } - (CrossSigningSetupBannerCell *)keyVerificationSetupBannerPrototypeCell { if (!_keyVerificationSetupBannerPrototypeCell) { _keyVerificationSetupBannerPrototypeCell = [self.recentsTableView dequeueReusableCellWithIdentifier:CrossSigningSetupBannerCell.defaultReuseIdentifier]; } return _keyVerificationSetupBannerPrototypeCell; } - (void)presentSecureBackupSetup { SecureBackupSetupCoordinatorBridgePresenter *keyBackupSetupCoordinatorBridgePresenter = [[SecureBackupSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession allowOverwrite:NO]; keyBackupSetupCoordinatorBridgePresenter.delegate = self; [keyBackupSetupCoordinatorBridgePresenter presentFrom:self animated:YES]; self.secureBackupSetupCoordinatorBridgePresenter = keyBackupSetupCoordinatorBridgePresenter; } - (void)addFabButton { plusButtonImageView = [self vc_addFABWithImage:AssetImages.plusFloatingAction.image target:self action:@selector(onPlusButtonPressed)]; } - (RecentsDataSourceMode)recentsDataSourceMode { return RecentsDataSourceModeHome; } #pragma mark - Override RecentsViewController - (void)displayList:(MXKRecentsDataSource *)listDataSource { [super displayList:listDataSource]; // Change the table data source. It must be the home view controller itself. self.recentsTableView.dataSource = self; // Keep a ref on the recents data source if ([listDataSource isKindOfClass:RecentsDataSource.class]) { recentsDataSource = (RecentsDataSource*)listDataSource; } } - (void)refreshCurrentSelectedCell:(BOOL)forceVisible { // Check whether the recents data source is correctly configured. if (recentsDataSource.recentsDataSourceMode != self.recentsDataSourceMode) { return; } // TODO: refreshCurrentSelectedCell //[super refreshCurrentSelectedCell:forceVisible]; } - (void)didTapOnSectionHeader:(UIGestureRecognizer*)gestureRecognizer { UIView *view = gestureRecognizer.view; NSInteger section = view.tag; if (selectedRoomId) { [self cancelEditionMode:YES]; } // Scroll to the top this section if ([self.recentsTableView numberOfRowsInSection:section] > 0) { [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; } // Scroll to the beginning the corresponding rooms collection. UITableViewCell *firstSectionCell = [self.recentsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; if (firstSectionCell && [firstSectionCell isKindOfClass:TableViewCellWithCollectionView.class]) { TableViewCellWithCollectionView *tableViewCell = (TableViewCellWithCollectionView*)firstSectionCell; if ([tableViewCell.collectionView numberOfItemsInSection:0] > 0) { [tableViewCell.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; } } } - (void)scrollToTop:(BOOL)animated { if (selectedRoomId) { [self cancelEditionMode:YES]; } [super scrollToTop:animated]; } - (void)onPlusButtonPressed { if (selectedRoomId) { [self cancelEditionMode:YES]; } [super onPlusButtonPressed]; } - (void)cancelEditionMode:(BOOL)forceRefresh { if (selectedRoomId) { // Ignore forceRefresh, a table refresh is forced at the end. [super cancelEditionMode:NO]; editedRoomId = selectedRoomId = nil; if (selectedCollectionViewContentOffset == -1) { selectedSection = -1; } // Else, do not reset the selectedSection here, // it is used during the table refresh to apply the original collection view offset. // Remove existing gesture recognizers [self.recentsTableView removeGestureRecognizer:horizontalSwipeGestureRecognizer]; horizontalSwipeGestureRecognizer = nil; [self.recentsTableView removeGestureRecognizer:verticalSwipeGestureRecognizer]; verticalSwipeGestureRecognizer = nil; self.recentsTableView.scrollEnabled = YES; [self refreshRecentsTable]; } } - (void)onMatrixSessionChange { [super onMatrixSessionChange]; [self updateEmptyView]; } - (void)startChat { if (recentsDataSource.currentSpace) { self.spaceMembersCoordinatorBridgePresenter = [[SpaceMembersCoordinatorBridgePresenter alloc] initWithUserSessionsService:[UserSessionsService shared] session:self.mainSession spaceId:self.dataSource.currentSpace.spaceId]; self.spaceMembersCoordinatorBridgePresenter.delegate = self; [self.spaceMembersCoordinatorBridgePresenter presentFrom:self animated:YES]; } else { [super startChat]; } } - (void)createNewRoom { if (recentsDataSource.currentSpace) { [recentsDataSource.currentSpace canAddRoomWithCompletion:^(BOOL canAddRoom) { if (canAddRoom) { [super createNewRoom]; } else { [[AppDelegate theDelegate] showAlertWithTitle:[VectorL10n roomRecentsCreateEmptyRoom] message:[VectorL10n spacesAddRoomMissingPermissionMessage]]; } }]; } else { [super createNewRoom]; } } #pragma mark - UITableViewDataSource // Table view cells on the home screen contain nested collection views with their own data source and state. // In order to preserve properties such as content offset of each collection view, the parent cells must // be directly associated with each section, so that when getting dequed by the table view, the correct cell // is reused, rather than cells getting randomly swapped around. - (void)registerCellsWithCollectionViews { for (NSNumber *section in self.sections) { NSString *cellIdentifier = [self cellIdentifierForSectionType:section.integerValue]; [self.recentsTableView registerClass:TableViewCellWithCollectionView.class forCellReuseIdentifier:cellIdentifier]; } } - (NSArray *)sections { return @[ @(RecentsDataSourceSectionTypeDirectory), @(RecentsDataSourceSectionTypeInvites), @(RecentsDataSourceSectionTypeFavorites), @(RecentsDataSourceSectionTypePeople), @(RecentsDataSourceSectionTypeConversation), @(RecentsDataSourceSectionTypeLowPriority), @(RecentsDataSourceSectionTypeServerNotice), @(RecentsDataSourceSectionTypeSuggestedRooms), @(RecentsDataSourceSectionTypeBreadcrumbs) ]; } - (NSString *)cellIdentifierForSectionType:(RecentsDataSourceSectionType)sectionType { // Create cell identifier unique to each semantic section, e.g. 'favorites' will have different cell // identifier to 'conversations'. return [NSString stringWithFormat:@"%@-%ld", TableViewCellWithCollectionView.defaultReuseIdentifier, sectionType]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the actual number of sections prepared in recents dataSource. return [recentsDataSource numberOfSectionsInTableView:tableView]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Edit the potential selected room (see `onCollectionViewCellLongPress`). editedRoomId = selectedRoomId; if ([recentsDataSource isSectionShrinkedAt:section]) { return 0; } else { // Each rooms section is represented by only one collection view except for the all chats section. NSInteger index = [recentsDataSource.sections sectionIndexForSectionType:RecentsDataSourceSectionTypeAllChats]; if (section == index) { return [self.dataSource tableView:tableView numberOfRowsInSection:section]; } return 1; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger index = [recentsDataSource.sections sectionIndexForSectionType:RecentsDataSourceSectionTypeAllChats]; if (indexPath.section == index) { return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; } RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:indexPath.section]; if ((sectionType == RecentsDataSourceSectionTypeConversation && !recentsDataSource.recentsListService.conversationRoomListData.counts.numberOfRooms) || (sectionType == RecentsDataSourceSectionTypePeople && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms) || (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner) || (sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) ) { return [recentsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; } NSString *cellIdentifier = [self cellIdentifierForSectionType:sectionType]; TableViewCellWithCollectionView *tableViewCell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath]; tableViewCell.collectionView.tag = indexPath.section; [tableViewCell.collectionView registerClass:RoomCollectionViewCell.class forCellWithReuseIdentifier:RoomCollectionViewCell.defaultReuseIdentifier]; tableViewCell.collectionView.delegate = self; tableViewCell.collectionView.dataSource = self; tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; if (editedRoomId) { UIColor *selectedColor = ThemeService.shared.theme.tintColor; UIColor *unselectedColor = ThemeService.shared.theme.tabBarUnselectedItemTintColor; // Disable collection scrolling during edition tableViewCell.collectionView.scrollEnabled = NO; if (indexPath.section == selectedSection) { // Show edition menu tableViewCell.editionViewHeightConstraint.constant = 60; tableViewCell.editionViewBottomConstraint.constant = 5; tableViewCell.editionView.hidden = NO; MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; // Update the edition menu content (Use the button tag to store the current value). tableViewCell.directChatButton.tag = room.isDirect; [tableViewCell.directChatButton addTarget:self action:@selector(onDirectChatButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; tableViewCell.directChatImageView.image = AssetImages.roomActionDirectChat.image; tableViewCell.directChatImageView.tintColor = room.isDirect ? selectedColor : unselectedColor; tableViewCell.notificationsButton.tag = room.isMute || room.isMentionsOnly; [tableViewCell.notificationsButton addTarget:self action:@selector(onNotificationsButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; if ([BuildSettings showNotificationsV2] && tableViewCell.notificationsButton.tag) { tableViewCell.notificationsImageView.image = AssetImages.roomActionNotificationMuted.image; } else { tableViewCell.notificationsImageView.image = AssetImages.roomActionNotification.image; } tableViewCell.notificationsImageView.tintColor = tableViewCell.notificationsButton.tag ? unselectedColor : selectedColor; // Get the room tag (use only the first one). MXRoomTag* currentTag = nil; if (room.accountData.tags) { NSArray* tags = room.accountData.tags.allValues; if (tags.count) { currentTag = tags[0]; } } tableViewCell.favouriteButton.tag = (currentTag && [kMXRoomTagFavourite isEqualToString:currentTag.name]); [tableViewCell.favouriteButton addTarget:self action:@selector(onFavouriteButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; tableViewCell.favouriteImageView.image = AssetImages.roomActionFavourite.image; tableViewCell.favouriteImageView.tintColor = tableViewCell.favouriteButton.tag ? selectedColor : unselectedColor; tableViewCell.priorityButton.tag = (currentTag && [kMXRoomTagLowPriority isEqualToString:currentTag.name]); [tableViewCell.priorityButton addTarget:self action:@selector(onPriorityButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; tableViewCell.priorityImageView.image = tableViewCell.priorityButton.tag ? AssetImages.roomActionPriorityHigh.image : AssetImages.roomActionPriorityLow.image; tableViewCell.priorityImageView.tintColor = unselectedColor; [tableViewCell.leaveButton addTarget:self action:@selector(onLeaveButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; tableViewCell.leaveImageView.image = AssetImages.roomActionLeave.image; tableViewCell.leaveImageView.tintColor = unselectedColor; } } return tableViewCell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger index = [recentsDataSource.sections sectionIndexForSectionType:RecentsDataSourceSectionTypeAllChats]; if (indexPath.section == index) { return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; } return NO; } #pragma mark - UITableView delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger index = [recentsDataSource.sections sectionIndexForSectionType:RecentsDataSourceSectionTypeAllChats]; if (indexPath.section == index) { return [super tableView:tableView heightForRowAtIndexPath:indexPath]; } RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:indexPath.section]; if ((sectionType == RecentsDataSourceSectionTypeConversation && !recentsDataSource.recentsListService.conversationRoomListData.counts.numberOfRooms) || (sectionType == RecentsDataSourceSectionTypePeople && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms)) { return [recentsDataSource cellHeightAtIndexPath:indexPath]; } else if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner || sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) { CGFloat height = 0.0; UITableViewCell *sizingCell; if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner) { SecureBackupBannerCell *secureBackupBannerCell = self.secureBackupBannerPrototypeCell; [secureBackupBannerCell configureFor:recentsDataSource.secureBackupBannerDisplay]; sizingCell = secureBackupBannerCell; } else if (sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) { sizingCell = self.keyVerificationSetupBannerPrototypeCell; } [sizingCell layoutIfNeeded]; CGSize fittingSize = UILayoutFittingCompressedSize; CGFloat tableViewWidth = CGRectGetWidth(tableView.frame); CGFloat safeAreaWidth = MAX(tableView.safeAreaInsets.left, tableView.safeAreaInsets.right); fittingSize.width = tableViewWidth - safeAreaWidth; height = [sizingCell systemLayoutSizeFittingSize:fittingSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel].height; return height; } // Retrieve the fixed height of the collection view cell used to display a room. CGFloat height = [RoomCollectionViewCell defaultCellSize].height + 1; // Check the conditions to display the edition menu if (editedRoomId && indexPath.section == selectedSection) { // Add the edition view height height += 65.0; } return height; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { // No header in key banner section RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:section]; if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner || sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) { return 0.0; } else { return [(RecentsDataSource *)self.dataSource heightForHeaderInSection:section]; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:indexPath.section]; if (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner) { switch (recentsDataSource.secureBackupBannerDisplay) { case SecureBackupBannerDisplaySetup: [self presentSecureBackupSetup]; break; default: break; } } else if (sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) { [self showCrossSigningSetup]; } else if (sectionType == RecentsDataSourceSectionTypeAllChats) { [super tableView:tableView didSelectRowAtIndexPath:indexPath]; } } #pragma mark - UICollectionViewDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [recentsDataSource tableView:self.recentsTableView numberOfRowsInSection:collectionView.tag]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { RoomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:RoomCollectionViewCell.defaultReuseIdentifier forIndexPath:indexPath]; id cellData = [recentsDataSource cellDataAtIndexPath:[NSIndexPath indexPathForRow:indexPath.item inSection:collectionView.tag]]; if (cellData) { [cell render:cellData]; cell.tag = indexPath.item; cell.collectionViewTag = collectionView.tag; if (selectedCollectionViewContentOffset != -1 && collectionView.tag == selectedSection) { if (collectionView.contentOffset.x != selectedCollectionViewContentOffset) { // Force here the content offset of the collection in which the edited cell is displayed. // Indeed because of the table view cell height change the collection view scrolls at the beginning by default (on iOS < 10). collectionView.contentOffset = CGPointMake(selectedCollectionViewContentOffset, 0) ; } if (editedRoomId) { // Scroll the collection view in order to fully display the edited cell. NSIndexPath *indexPath = [self.dataSource cellIndexPathWithRoomId:editedRoomId andMatrixSession:self.mainSession]; indexPath = [NSIndexPath indexPathForItem:indexPath.item inSection:0]; [collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionNone animated:YES]; selectedCollectionViewContentOffset = collectionView.contentOffset.x; } else { // The edition mode is left now, remove the last stored values. selectedSection = -1; selectedCollectionViewContentOffset = -1; } } // Edition mode? if (editedRoomId) { UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellTap:)]; [cell addGestureRecognizer:tapGesture]; if ([cellData.roomIdentifier isEqualToString:editedRoomId]) { cell.editionArrowView.hidden = NO; } } else { if (@available(iOS 13.0, *)) { // Use context menu instead } else { // Add long tap gesture recognizer. UILongPressGestureRecognizer *cellLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellLongPress:)]; [cell addGestureRecognizer:cellLongPressGesture]; } } } cell.backgroundColor = ThemeService.shared.theme.backgroundColor; return cell; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { NSInteger collectionViewSection = indexPath.section; if (collectionView.numberOfSections <= collectionViewSection) { return; } NSInteger numberOfItemsInSection = [collectionView numberOfItemsInSection:collectionViewSection]; if (indexPath.item != numberOfItemsInSection - 1) { return; } [self.collectionViewPaginationThrottler throttle:^{ NSInteger tableViewSection = collectionView.tag; [self->recentsDataSource paginateInSection:tableViewSection]; }]; } #pragma mark - UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (self.delegate) { RoomCollectionViewCell *roomCollectionViewCell = (RoomCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath]; id renderedCellData = (id)roomCollectionViewCell.renderedCellData; if (renderedCellData.isSuggestedRoom) { [self.delegate recentListViewController:self didSelectSuggestedRoom:renderedCellData.roomSummary.spaceChildInfo from:roomCollectionViewCell]; } else { [self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomIdentifier inMatrixSession:renderedCellData.mxSession]; } } // Hide the keyboard when user select a room // do not hide the searchBar until the view controller disappear // on tablets / iphone 6+, the user could expect to search again while looking at a room [self.recentsSearchBar resignFirstResponder]; } #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return [RoomCollectionViewCell defaultCellSize]; } #pragma mark - Gesture Recognizer - (void)onCollectionViewCellLongPress:(UIGestureRecognizer*)gestureRecognizer { RoomCollectionViewCell *selectedCell; if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { UIView *view = gestureRecognizer.view; if ([view isKindOfClass:[RoomCollectionViewCell class]]) { selectedCell = (RoomCollectionViewCell*)view; MXRoom* room = [self.dataSource getRoomAtIndexPath:[NSIndexPath indexPathForRow:selectedCell.tag inSection:selectedCell.collectionViewTag]]; if (room) { // Display no action for the invited room if (room.summary.membership == MXMembershipInvite) { return; } // Store the identifier of the room related to the edited cell. selectedRoomId = room.roomId; // Store the concerned section selectedCollectionViewContentOffset = -1; selectedSection = selectedCell.collectionViewTag; // Store the current content offset of the selected collection before refreshing. NSIndexPath *tableViewCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:selectedSection]; TableViewCellWithCollectionView *tableViewCellWithCollectionView = [self.recentsTableView cellForRowAtIndexPath:tableViewCellIndexPath]; CGFloat selectedCollectionViewContentOffsetCopy = tableViewCellWithCollectionView.collectionView.contentOffset.x; [self refreshRecentsTable]; // Make visible the edited cell tableViewCellWithCollectionView = [self.recentsTableView cellForRowAtIndexPath:tableViewCellIndexPath]; NSIndexPath *collectionViewCellIndexPath = [self.dataSource cellIndexPathWithRoomId:selectedRoomId andMatrixSession:room.mxSession]; collectionViewCellIndexPath = [NSIndexPath indexPathForItem:collectionViewCellIndexPath.item inSection:0]; UICollectionViewCell *roomCollectionViewCell = [tableViewCellWithCollectionView.collectionView cellForItemAtIndexPath:collectionViewCellIndexPath]; if (roomCollectionViewCell) { [tableViewCellWithCollectionView.collectionView scrollRectToVisible:roomCollectionViewCell.frame animated:YES]; } else { // On iOS < 10, the collection view scrolls to the beginning during the table refresh. // We store here the actual content offset, used during the collection view loading. selectedCollectionViewContentOffset = selectedCollectionViewContentOffsetCopy; } [self.recentsTableView scrollRectToVisible:tableViewCellWithCollectionView.frame animated:YES]; // Disable table view scrolling, and defined the swipe gesture recognizers used to cancel the edition mode self.recentsTableView.scrollEnabled = NO; horizontalSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onTableViewSwipe:)]; [horizontalSwipeGestureRecognizer setDirection:(UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight)]; [self.recentsTableView addGestureRecognizer:horizontalSwipeGestureRecognizer]; verticalSwipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onTableViewSwipe:)]; [verticalSwipeGestureRecognizer setDirection:(UISwipeGestureRecognizerDirectionUp | UISwipeGestureRecognizerDirectionDown)]; [self.recentsTableView addGestureRecognizer:verticalSwipeGestureRecognizer]; } } } } - (void)onCollectionViewCellTap:(UIGestureRecognizer*)gestureRecognizer { [self cancelEditionMode:YES]; } - (void)onTableViewSwipe:(UIGestureRecognizer*)gestureRecognizer { [self cancelEditionMode:YES]; } #pragma mark - Action - (IBAction)onDirectChatButtonPressed:(id)sender { UIButton *button = (UIButton*)sender; [self makeDirectEditedRoom:!button.tag]; } - (IBAction)onNotificationsButtonPressed:(id)sender { if ([BuildSettings showNotificationsV2]) { [self changeEditedRoomNotificationSettings]; } else { UIButton *button = (UIButton*)sender; [self muteEditedRoomNotifications:!button.tag]; } } - (IBAction)onFavouriteButtonPressed:(id)sender { UIButton *button = (UIButton*)sender; if (button.tag) { [self updateEditedRoomTag:nil]; } else { [self updateEditedRoomTag:kMXRoomTagFavourite]; } } - (IBAction)onPriorityButtonPressed:(id)sender { UIButton *button = (UIButton*)sender; if (button.tag) { [self updateEditedRoomTag:nil]; } else { [self updateEditedRoomTag:kMXRoomTagLowPriority]; } } - (IBAction)onLeaveButtonPressed:(id)sender { [self leaveEditedRoom]; } #pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate - (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter { [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.secureBackupSetupCoordinatorBridgePresenter = nil; } - (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter { [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.secureBackupSetupCoordinatorBridgePresenter = nil; } #pragma mark - Cross-signing setup - (void)showCrossSigningSetup { [self setupCrossSigningWithTitle:[VectorL10n crossSigningSetupBannerTitle] message:[VectorL10n securitySettingsUserPasswordDescription] success:^{ } failure:^(NSError *error) { }]; } - (void)setupCrossSigningWithTitle:(NSString*)title message:(NSString*)message success:(void (^)(void))success failure:(void (^)(NSError *error))failure { [self startActivityIndicator]; self.view.userInteractionEnabled = NO; MXWeakify(self); void (^animationCompletion)(void) = ^void () { MXStrongifyAndReturnIfNil(self); [self stopActivityIndicator]; self.view.userInteractionEnabled = YES; [self.crossSigningSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{}]; self.crossSigningSetupCoordinatorBridgePresenter = nil; }; CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter = [[CrossSigningSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; [crossSigningSetupCoordinatorBridgePresenter presentWith:title message:message from:self animated:YES success:^{ animationCompletion(); // TODO: Remove this line and refresh key verification setup banner by listening to a local notification cross-signing state change (Add this behavior into the SDK). [self->recentsDataSource setDelegate:self andRecentsDataSourceMode:self.recentsDataSourceMode]; [self refreshRecentsTable]; success(); } cancel:^{ animationCompletion(); failure(nil); } failure:^(NSError * _Nonnull error) { animationCompletion(); [self refreshRecentsTable]; [[AppDelegate theDelegate] showErrorAsAlert:error]; failure(error); }]; self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter; } #pragma mark - Empty view management - (void)updateEmptyView { MXUser *myUser = self.mainSession.myUser; NSString *displayName = myUser.displayname ?: myUser.userId; displayName = displayName ?: @""; NSString *appName; if(![BWIBuildSettings.shared.secondaryAppName isEqualToString:@""]) { appName = BWIBuildSettings.shared.secondaryAppName; } else { appName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; } NSString *title = [VectorL10n homeEmptyViewTitle:appName :displayName]; [self.emptyView fillWith:[self emptyViewArtwork] title:title informationText:[VectorL10n homeEmptyViewInformation]]; } - (UIImage*)emptyViewArtwork { if (ThemeService.shared.isCurrentThemeDark) { return AssetImages.homeEmptyScreenArtworkDark.image; } else { return AssetImages.homeEmptyScreenArtwork.image; } } - (BOOL)shouldShowEmptyView { // Do not present empty screen while searching if (recentsDataSource.searchPatternsList.count) { return NO; } // Check if some banners should be displayed if ([recentsDataSource.sections contains:RecentsDataSourceSectionTypeSecureBackupBanner] || [recentsDataSource.sections contains:RecentsDataSourceSectionTypeCrossSigningBanner] || [recentsDataSource.sections contains:RecentsDataSourceSectionTypeFeatureBanner]) // bwi: #6114 fix top banner for new users who have not yet joined a room { return NO; } // Otherwise check the number of items to display return recentsDataSource.totalVisibleItemCount == 0; } #pragma mark - SpaceMembersCoordinatorBridgePresenterDelegate - (void)spaceMembersCoordinatorBridgePresenterDelegateDidComplete:(SpaceMembersCoordinatorBridgePresenter *)coordinatorBridgePresenter { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ self.spaceMembersCoordinatorBridgePresenter = nil; }]; } #pragma mark - Context Menu - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:indexPath.section]; if (sectionType == RecentsDataSourceSectionTypeAllChats) { return [super tableView:tableView contextMenuConfigurationForRowAtIndexPath:indexPath point:point]; } return nil; } - (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { id cellData = [recentsDataSource cellDataAtIndexPath:[NSIndexPath indexPathForRow:indexPath.item inSection:collectionView.tag]]; UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; if (!cellData || !cell) { return nil; } return [self.contextMenuProvider contextMenuConfigurationWith:cellData from:cell session:self.dataSource.mxSession]; } - (void)collectionView:(UICollectionView *)collectionView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)) { NSString *roomId = [self.contextMenuProvider roomIdFrom:configuration.identifier]; if (!roomId) { self.recentsUpdateEnabled = YES; return; } [animator addCompletion:^{ self.recentsUpdateEnabled = YES; [self showRoomWithRoomId:roomId inMatrixSession:self.mainSession]; }]; } - (UITargetedPreview *)collectionView:(UICollectionView *)collectionView previewForDismissingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)) { self.recentsUpdateEnabled = YES; return nil; } @end