// // Copyright 2022 New Vector 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. // // swiftlint:disable file_length import UIKit import SwiftUI import Reusable protocol AllChatsViewControllerDelegate: AnyObject { func allChatsViewControllerDidCompleteAuthentication(_ allChatsViewController: AllChatsViewController) func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomWithParameters roomNavigationParameters: RoomNavigationParameters, completion: @escaping () -> Void) func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectContact contact: MXKContact, with presentationParameters: ScreenPresentationParameters) } class AllChatsViewController: HomeViewController { // MARK: - Class methods static override func nib() -> UINib! { return UINib(nibName: String(describing: self), bundle: Bundle(for: self.classForCoder())) } static override func instantiate() -> Self { let storyboard = UIStoryboard(name: "Main", bundle: .main) guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else { fatalError("No view controller of type \(self) in the main storyboard") } return viewController } // MARK: - Properties weak var allChatsDelegate: AllChatsViewControllerDelegate? // MARK: - Private private let searchController = UISearchController(searchResultsController: nil) private let spaceActionProvider = AllChatsSpaceActionProvider() private let editActionProvider = AllChatsEditActionProvider() private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter? private var childCoordinators: [Coordinator] = [] private let tableViewPaginationThrottler = MXThrottler(minimumDelay: 0.1) private let reviewSessionAlertSnoozeController = ReviewSessionAlertSnoozeController() // bwi: new feature banner height private var featureBannerViewHeight: CGFloat = 0 // bwi: application privacy modal for matomo consent private var navigationBar: UINavigationController? private var showMatomoConsentAlertOnCloseModal: Bool = false private var bannerView: UIView? { didSet { bannerView?.translatesAutoresizingMaskIntoConstraints = false set(tableHeadeView: bannerView) } } private var isOnboardingCoordinatorPreparing: Bool = false // bwi: 4807 private var floatingButton: UIButton? private var theme: Theme { ThemeService.shared().theme } @IBOutlet private var toolbar: UIToolbar! private var isToolbarHidden: Bool = false { didSet { if isViewLoaded { toolbar.transform = isToolbarHidden ? CGAffineTransform(translationX: 0, y: 2 * toolbarHeight) : .identity self.view.layoutIfNeeded() } } } private func setToolbarHidden(_ isHidden: Bool, animated: Bool) { if BWIBuildSettings.shared.enableAllChatsToolbar { UIView.animate(withDuration: animated ? 0.3 : 0) { self.isToolbarHidden = isHidden } } } // MARK: - SplitViewMasterViewControllerProtocol // References on the currently selected room private(set) var selectedRoomId: String? private(set) var selectedEventId: String? private(set) var selectedRoomSession: MXSession? private(set) var selectedRoomPreviewData: RoomPreviewData? // References on the currently selected contact private(set) var selectedContact: MXKContact? // Reference to the current onboarding flow. It is always nil unless the flow is being presented. private(set) var onboardingCoordinatorBridgePresenter: OnboardingCoordinatorBridgePresenter? // Tell whether the onboarding screen is preparing. private(set) var isOnboardingInProgress: Bool = false private var toolbarHeight: CGFloat = 0 private weak var roomFilterButton: UIButton? // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() editActionProvider.delegate = self spaceActionProvider.delegate = self recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue recentsTableView.clipsToBounds = false recentsTableView.register(RecentEmptySectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySectionTableViewCell.reuseIdentifier) recentsTableView.register(RecentEmptySpaceSectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySpaceSectionTableViewCell.reuseIdentifier) recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier) recentsTableView.register(FeatureBannerViewCell.self, forCellReuseIdentifier: "featureBanner") recentsTableView.contentInsetAdjustmentBehavior = .automatic toolbarHeight = toolbar.frame.height emptyViewBottomAnchor = toolbar.topAnchor if BWIBuildSettings.shared.useNewBumColors { // bwi: #4883 toolbar.tintColor = ThemeService.shared().theme.tintColor toolbar.barTintColor = ThemeService.shared().theme.backgroundColor } else { toolbar.tintColor = theme.colors.accent } // bwi: 4807 - hide the toolbar and show a floating button for room create instead if !BWIBuildSettings.shared.enableAllChatsToolbar { // no toolbar then use a floating button instead floatingButton = UIButton(frame: CGRect(x: 0, y: 0, width: 56, height: 56)) updateFloatingButton() if let floatingButton = floatingButton { view.addSubview(floatingButton) } // set constraint to make the floating button stay in the lower right corner floatingButton?.translatesAutoresizingMaskIntoConstraints = false floatingButton?.widthAnchor.constraint(equalToConstant: 56).isActive = true floatingButton?.heightAnchor.constraint(equalToConstant: 56).isActive = true floatingButton?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16).isActive = true floatingButton?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40).isActive = true } if BWIBuildSettings.shared.roomFiltersToggle { updateNewFilterSearchAndToggleButton() } updateUI() navigationItem.largeTitleDisplayMode = .never navigationController?.navigationBar.prefersLargeTitles = false if !BWIBuildSettings.shared.roomFiltersToggle { searchController.obscuresBackgroundDuringPresentation = false searchController.searchResultsUpdater = self searchController.delegate = self } NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.updateBadgeButton), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil) // bwi: 4769 self.registerThemeServiceDidChangeThemeNotification() } // bwi: 4806 private func updateNewFilterSearchAndToggleButton() { let customHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: recentsTableView.frame.width, height: 50)) let searchBar = UISearchBar(frame: CGRect(x: 8, y: 0, width: customHeaderView.frame.width - 48, height: 50)) let button = UIButton(frame: CGRect(x: customHeaderView.frame.width - 48 + 8, y: 0, width: 40, height: 50)) if ThemeService.shared().isCurrentThemeDark() { button.setImage(Asset.Images.roomFilterToggleDarkOff.image, for: .normal) button.setImage(Asset.Images.roomFilterToggleDarkOn.image, for: .selected) } else { button.setImage(Asset.Images.roomFilterToggleLightOff.image, for: .normal) button.setImage(Asset.Images.roomFilterToggleLightOn.image, for: .selected) } let settings = AllChatsLayoutSettingsManager.shared.allChatLayoutSettings let areFiltersVisible = !settings.filters.isEmpty button.isSelected = !AllChatsLayoutSettingsManager.shared.allChatLayoutSettings.filters.isEmpty button.addTarget(self, action: #selector(onFilterToggleTapped), for: .touchUpInside) roomFilterButton = button // keep a reference to handle theme changes searchBar.placeholder = BWIL10n.allChatsSearchbarPrompt searchBar.backgroundImage = UIImage() // this hides the separator lines above and below searchBar.delegate = self customHeaderView.addSubview(searchBar) customHeaderView.addSubview(button) recentsTableView.tableHeaderView = customHeaderView // set some constraints for landscape and iPad customHeaderView.translatesAutoresizingMaskIntoConstraints = false let constraints = [ searchBar.leadingAnchor.constraint(equalTo: customHeaderView.leadingAnchor), searchBar.centerYAnchor.constraint(equalTo: button.centerYAnchor), searchBar.widthAnchor.constraint(equalToConstant: 40), searchBar.heightAnchor.constraint(equalToConstant: 40), button.leadingAnchor.constraint(equalTo: searchBar.trailingAnchor), button.trailingAnchor.constraint(equalTo: customHeaderView.trailingAnchor), button.topAnchor.constraint(equalTo: customHeaderView.topAnchor), button.bottomAnchor.constraint(equalTo: customHeaderView.bottomAnchor), customHeaderView.centerXAnchor.constraint(equalTo: recentsTableView.centerXAnchor), customHeaderView.widthAnchor.constraint(equalTo: recentsTableView.widthAnchor), customHeaderView.topAnchor.constraint(equalTo: recentsTableView.topAnchor) ] NSLayoutConstraint.activate(constraints) } @objc func onSearchTextChanged(searchBar: UISearchBar) { guard let searchText = searchBar.text, !searchText.isEmpty else { self.dataSource?.search(withPatterns: nil) return } self.dataSource?.search(withPatterns: [searchText]) } @objc func onFilterToggleTapped() { let settings = AllChatsLayoutSettingsManager.shared.allChatLayoutSettings let areFiltersVisible = !settings.filters.isEmpty let newSettings = AllChatsLayoutSettings(sections: settings.sections, filters: areFiltersVisible ? [] : [.unreads, .favourites, .people], sorting: settings.sorting) AllChatsLayoutSettingsManager.shared.allChatLayoutSettings = newSettings Analytics.shared.trackInteraction(areFiltersVisible ? .allChatsFiltersDisabled : .allChatsFiltersEnabled) roomFilterButton?.isSelected = !AllChatsLayoutSettingsManager.shared.allChatLayoutSettings.filters.isEmpty } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // bwi: 4769 if BWIBuildSettings.shared.useNewBumColors { self.toolbar.tintColor = theme.tintColor self.toolbar.barTintColor = theme.backgroundColor } else { self.toolbar.tintColor = theme.colors.accent } // bwi: 4807 - hide the toolbar and show a floating button for room create instead if !BWIBuildSettings.shared.enableAllChatsToolbar { toolbar.transform = CGAffineTransform(translationX: 0, y: 2 * toolbarHeight) self.view.layoutIfNeeded() } if BWIBuildSettings.shared.roomFiltersToggle { if ThemeService.shared().isCurrentThemeDark() { roomFilterButton?.setImage(Asset.Images.roomFilterToggleDarkOff.image, for: .normal) roomFilterButton?.setImage(Asset.Images.roomFilterToggleDarkOn.image, for: .selected) } else { roomFilterButton?.setImage(Asset.Images.roomFilterToggleLightOff.image, for: .normal) roomFilterButton?.setImage(Asset.Images.roomFilterToggleLightOn.image, for: .selected) } } else { if self.navigationItem.searchController == nil { self.navigationItem.searchController = searchController } } NotificationCenter.default.addObserver(self, selector: #selector(self.spaceListDidChange), name: MXSpaceService.didInitialise, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.spaceListDidChange), name: MXSpaceService.didBuildSpaceGraph, object: nil) set(tableHeadeView: self.bannerView) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Check whether we're not logged in let authIsShown: Bool // bwi: (#4394) handling of the case of canceled login during pincode stage. if MXKAccountManager.shared().accounts.isEmpty || !PinCodePreferences.shared.isPinSet && PinCodePreferences.shared.forcePinProtection { showOnboardingFlow() authIsShown = true } else { // Display a login screen if the account is soft logout // Note: We support only one account if let account = MXKAccountManager.shared().accounts.first, account.isSoftLogout { showSoftLogoutOnboardingFlow(with: account.mxCredentials) authIsShown = true } else { authIsShown = false } } guard !authIsShown else { return } AppDelegate.theDelegate().checkAppVersion() } func presentFederationIntroductionSheet() { guard BWIBuildSettings.shared.showFederationIntroduction else { return } guard BWIBuildSettings.shared.isFederationEnabled else { return } guard self.mainSession.homeserverWellknown.shouldShowFederationIntroduction() else { return } let notificationService = BWIAccountNotificationService(mxSession: self.mainSession) guard notificationService.showFederationIntroductionFlag() else { return } let viewController = UIHostingController(rootView: IntroduceFederationView().environmentObject(BWIThemeService.shared)) viewController.isModalInPresentation = true present(viewController, animated: true) { notificationService.setShowFederationIntroductionFlag(false) } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate { context in self.recentsTableView?.tableHeaderView?.layoutIfNeeded() // self.recentsTableView?.tableHeaderView = self.recentsTableView?.tableHeaderView } } // bwi: 4769 private func registerThemeServiceDidChangeThemeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) } // bwi: 4769 @objc private func themeDidChange() { self.update(with: ThemeService.shared().theme) } // MARK: - Public func switchSpace(withId spaceId: String?) { searchController.isActive = false guard let spaceId = spaceId else { dataSource?.currentSpace = nil updateUI() return } guard let space = self.mainSession.spaceService.getSpace(withId: spaceId) else { MXLog.warning("[AllChatsViewController] switchSpace: no space found with id \(spaceId)") return } dataSource?.currentSpace = space updateUI() self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) } override var recentsDataSourceMode: RecentsDataSourceMode { .allChats } override func addMatrixSession(_ mxSession: MXSession!) { super.addMatrixSession(mxSession) if let dataSource = dataSource, !dataSource.mxSessions.contains(where: { $0 as? MXSession == mxSession }) { dataSource.addMatrixSession(mxSession) // Setting the delegate is required to send a RecentsViewControllerDataReadyNotification. // Without this, when clearing the cache we end up with an infinite green spinner. (dataSource as? RecentsDataSource)?.setDelegate(self, andRecentsDataSourceMode: recentsDataSourceMode) } else { initDataSource() } } override func removeMatrixSession(_ mxSession: MXSession!) { super.removeMatrixSession(mxSession) guard let dataSource = dataSource else { return } dataSource.removeMatrixSession(mxSession) if dataSource.mxSessions.isEmpty { // The user logged out -> we need to reset the data source displayList(nil) } } private func initDataSource() { guard self.dataSource == nil, let mainSession = self.mxSessions.first as? MXSession else { return } MXLog.debug("[AllChatsViewController] initDataSource") let recentsListService = RecentsListService(withSession: mainSession) let recentsDataSource = RecentsDataSource(matrixSession: mainSession, recentsListService: recentsListService) displayList(recentsDataSource) recentsDataSource?.setDelegate(self, andRecentsDataSourceMode: self.recentsDataSourceMode) } @objc private func spaceListDidChange() { guard self.editActionProvider.shouldUpdate(with: self.mainSession, parentSpace: self.dataSource?.currentSpace) else { return } updateUI() } @objc private func addFabButton() { // Nothing to do. We don't need FAB } @objc private func sections() -> Array { return [ RecentsDataSourceSectionType.directory.rawValue, RecentsDataSourceSectionType.invites.rawValue, RecentsDataSourceSectionType.favorites.rawValue, RecentsDataSourceSectionType.people.rawValue, RecentsDataSourceSectionType.allChats.rawValue, RecentsDataSourceSectionType.lowPriority.rawValue, RecentsDataSourceSectionType.serverNotice.rawValue, RecentsDataSourceSectionType.suggestedRooms.rawValue, RecentsDataSourceSectionType.breadcrumbs.rawValue, RecentsDataSourceSectionType.featureBanner.rawValue ] } override func startActivityIndicator() { super.startActivityIndicator() } // bwi: show alert again when modal is dismissed (button press) @objc func bwiCloseModal() { showMatomoConsentAlertOnCloseModal = false navigationBar?.dismiss(animated: true) bwiPresentMatomoConsentAlert() } func bwiPresentMatomoConsentAlert() { let alert = UIAlertController(title: BWIL10n.bwiAnalyticsAlertTitle, message: BWIL10n.bwiAnalyticsAlertBody(AppInfo.current.displayName), preferredStyle: .alert) alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertInfoButton, style: .default, handler: { [self] action in if let defaultURL = URL(string: BWIBuildSettings.shared.applicationPrivacyPolicyWithMatomoSectionUrlString) { if BWIBuildSettings.shared.bwiUseWellKnownPrivacyPolicyLink { guard let wellKnownDataPrivacyURL = URL(string: self.mainSession.homeserverWellknown.dataPrivacyURL() ?? ""), let defaultHost = defaultURL.host else { UIApplication.shared.open(defaultURL) return } if !wellKnownDataPrivacyURL.absoluteString.contains(defaultHost) { UIApplication.shared.open(wellKnownDataPrivacyURL) } else { UIApplication.shared.open(defaultURL) } } else { UIApplication.shared.open(defaultURL) } } showMatomoConsentAlertOnCloseModal = true })) alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertCancelButton, style: .default, handler: { action in BWIAnalytics.sharedTracker.running = false // bwi: 5706 show federation announcement promt self.bwiCheckForFederationAnnouncementPromt() // bwi: 5660 introduce federation self.presentFederationIntroductionSheet() })) alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertOkButton, style: .cancel, handler: { action in BWIAnalytics.sharedTracker.running = true BWIAnalytics.sharedTracker.trackBwiValue(0, "General", "ConsentGiven", "popup") // bwi: 5706 show federation announcement promt self.bwiCheckForFederationAnnouncementPromt() // bwi: 5660 introduce federation self.presentFederationIntroductionSheet() })) self.present(alert, animated: true) } // bwi: 5706 show federation announcement promt func bwiCheckForFederationAnnouncementPromt() { guard self.mainSession.homeserverWellknown.shouldShowFederationAnnouncement() else { return } let notificationService = BWIAccountNotificationService(mxSession: self.mainSession) guard notificationService.showFederationAnnouncementFlag() else { return } let viewController = UIHostingController(rootView: FederationAnnouncementView().environmentObject(BWIThemeService.shared)) viewController.isModalInPresentation = true viewController.modalPresentationStyle = .formSheet present(viewController, animated: true) { notificationService.setShowFederationAnnouncementFlag(false) } } // MARK: - Actions @objc private func showSpaceSelectorAction(sender: AnyObject) { Analytics.shared.viewRoomTrigger = .roomList let currentSpaceId = dataSource?.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true) spaceSelectorBridgePresenter.present(from: self, animated: true) spaceSelectorBridgePresenter.delegate = self self.spaceSelectorBridgePresenter = spaceSelectorBridgePresenter } // MARK: - UITableViewDataSource private func sectionType(forSectionAt index: Int) -> RecentsDataSourceSectionType? { guard let recentsDataSource = dataSource as? RecentsDataSource else { return nil } return recentsDataSource.sections.sectionType(forSectionIndex: index) } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let sectionType = sectionType(forSectionAt: section), sectionType == .invites else { return super.tableView(tableView, numberOfRowsInSection: section) } if sectionType == .featureBanner { return 1 } return dataSource?.tableView(tableView, numberOfRowsInSection: section) ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { return super.tableView(tableView, cellForRowAt: indexPath) } // bwi: feature banner cell if sectionType == .featureBanner { guard let cell = tableView.dequeueReusableCell(withIdentifier: "featureBanner", for: indexPath) as? FeatureBannerViewCell else { return UITableViewCell() } cell.selectionStyle = .none cell.setupView(parent: self, rootView: FeatureBannerView(delegate: cell)) featureBannerViewHeight = cell.calculateHeight() return cell } guard let dataSource = dataSource else { MXLog.failure("Missing data source") return UITableViewCell() } return dataSource.tableView(tableView, cellForRowAt: indexPath) } // MARK: - UITableViewDelegate override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { return super.tableView(tableView, heightForRowAt: indexPath) } if sectionType == .featureBanner { return featureBannerViewHeight } else { return dataSource?.cellHeight(at: indexPath) ?? 0 } } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { super.tableView(tableView, didSelectRowAt: indexPath) return } if sectionType == .invites { showRoomInviteList() } else { // bwi: feature banner is not selectable return } } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { super.tableView(tableView, willDisplay: cell, forRowAt: indexPath) guard let recentsDataSource = dataSource as? RecentsDataSource else { return } let sectionType = recentsDataSource.sections.sectionType(forSectionIndex: indexPath.section) // We need to trottle a bit earlier so the next section is not visible even if the tableview scrolls faster guard sectionType == .allChats, let numberOfRowsInSection = recentsDataSource.recentsListService.allChatsRoomListData?.counts.numberOfRooms, indexPath.row == numberOfRowsInSection - 4 else { return } tableViewPaginationThrottler.throttle { recentsDataSource.paginate(inSection: indexPath.section) } } // MARK: - Toolbar animation private var initialScrollPosition: Double = 0 private func scrollPosition(of scrollView: UIScrollView) -> Double { return scrollView.contentOffset.y + scrollView.adjustedContentInset.top } override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard scrollView == recentsTableView else { return } initialScrollPosition = scrollPosition(of: scrollView) } override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) guard scrollView == recentsTableView else { return } let scrollPosition = scrollPosition(of: scrollView) if !self.recentsTableView.isDragging && scrollPosition == 0 && self.isToolbarHidden == true { self.setToolbarHidden(false, animated: true) } guard self.recentsTableView.isDragging else { return } guard scrollPosition > 0 && scrollPosition < self.recentsTableView.contentSize.height - self.recentsTableView.bounds.height else { return } let isToolBarHidden: Bool = scrollPosition - initialScrollPosition > 0 if isToolBarHidden != self.isToolbarHidden { self.setToolbarHidden(isToolBarHidden, animated: true) } } // MARK: - Empty view management override func updateEmptyView() { guard let mainSession = self.mainSession else { return } let title: String let informationText: String if let currentSpace = self.dataSource?.currentSpace { title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayName ?? VectorL10n.spaceTag) informationText = VectorL10n.allChatsEmptySpaceInformation } else { let myUser = mainSession.myUser let displayName = (myUser?.displayName ?? myUser?.userId) ?? "" let appName = AppInfo.current.displayName title = VectorL10n.homeEmptyViewTitle(appName, displayName) informationText = VectorL10n.allChatsEmptyViewInformation } self.emptyView?.fill(with: emptyViewArtwork, title: title, informationText: informationText) // BWI: #5307 if let floatingButton = floatingButton, let emptyView = self.emptyView { if emptyView.superview != nil { view.insertSubview(floatingButton, aboveSubview: self.emptyView) } } } private var emptyViewArtwork: UIImage { if self.dataSource?.currentSpace == nil { return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.allChatsEmptyScreenArtworkDark.image : Asset.Images.allChatsEmptyScreenArtwork.image } else { return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.allChatsEmptySpaceArtworkDark.image : Asset.Images.allChatsEmptySpaceArtwork.image } } override func shouldShowEmptyView() -> Bool { let shouldShowEmptyView = super.shouldShowEmptyView() && !AllChatsLayoutSettingsManager.shared.hasAnActiveFilter if !BWIBuildSettings.shared.roomFiltersToggle { if shouldShowEmptyView { self.navigationItem.searchController = nil navigationItem.largeTitleDisplayMode = .never } else { self.navigationItem.searchController = searchController navigationItem.largeTitleDisplayMode = .automatic } } return shouldShowEmptyView } // MARK: - Theme management override func userInterfaceThemeDidChange() { super.userInterfaceThemeDidChange() guard self.toolbarItems != nil else { return } self.update(with: theme) } private func update(with theme: Theme) { // bwi: 4769 if BWIBuildSettings.shared.useNewBumColors { toolbar.tintColor = ThemeService.shared().theme.tintColor toolbar.barTintColor = ThemeService.shared().theme.backgroundColor // BWI: #4966 for barButtonItem in toolbar.items ?? [UIBarButtonItem]() { barButtonItem.tintColor = ThemeService.shared().theme.tintColor } UIToolbar.appearance().tintColor = ThemeService.shared().theme.tintColor UIToolbar.appearance().barTintColor = ThemeService.shared().theme.backgroundColor } else { self.navigationController?.toolbar?.tintColor = theme.colors.accent } // bwi: 4807 updateFloatingButton() } private func updateFloatingButton() { if ThemeService.shared().isCurrentThemeDark() { floatingButton?.setImage(Asset.Images.buttonNewDark.image, for: .normal) } else { floatingButton?.setImage(Asset.Images.buttonNewLight.image, for: .normal) } } // MARK: - Private private func set(tableHeadeView: UIView?) { guard let tableView = recentsTableView else { return } // tableView.tableHeaderView = tableHeadeView // tableView.tableHeaderView?.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true // tableView.tableHeaderView?.layoutIfNeeded() // tableView.tableHeaderView = self.recentsTableView?.tableHeaderView } @objc private func setupEditOptions() { guard let currentSpace = self.dataSource?.currentSpace else { updateRightNavigationItem(with: AllChatsActionProvider().menu) return } updateRightNavigationItem(with: spaceActionProvider.updateMenu(with: mainSession, space: currentSpace) { [weak self] menu in self?.updateRightNavigationItem(with: menu) }) } private func updateUI() { let currentSpace = self.dataSource?.currentSpace self.title = currentSpace?.summary?.displayName ?? VectorL10n.allChatsTitle setupEditOptions() let menu = editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in if BWIBuildSettings.shared.enableAllChatsToolbar { self?.updateToolbar(with: menu) } else { } }) if BWIBuildSettings.shared.enableAllChatsToolbar { updateToolbar(with: menu) } else { updateFloatingButton() self.floatingButton?.menu = menu self.floatingButton?.showsMenuAsPrimaryAction = true } updateEmptyView() updateBadgeButton() } private func updateRightNavigationItem(with menu: UIMenu) { // bwi 4704 - hide right navigation bar button if BWIBuildSettings.shared.showAllChatsFilterMenu { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu) } } private lazy var spacesButton: BadgedBarButtonItem = { let innerButton = UIButton(type: .system) innerButton.accessibilityLabel = VectorL10n.spaceSelectorTitle innerButton.addTarget(self, action: #selector(self.showSpaceSelectorAction(sender:)), for: .touchUpInside) innerButton.setImage(Asset.Images.allChatsSpacesIcon.image, for: .normal) return BadgedBarButtonItem(withBaseButton: innerButton, theme: theme) }() @objc private func updateBadgeButton() { guard isViewLoaded, let session = mainSession else { return } let notificationCount = session.spaceService.missedNotificationsCount let hasSpaceInvite = session.spaceService.hasSpaceInvite let isBadgeHighlighed = session.spaceService.hasHighlightNotification || hasSpaceInvite let badgeValue: String switch notificationCount { case 0: badgeValue = hasSpaceInvite ? "!" : "0" case (1 ... Constants.spacesButtonMaxCount): badgeValue = "\(notificationCount)" default: badgeValue = "\(Constants.spacesButtonMaxCount)+" } spacesButton.badgeText = badgeValue spacesButton.badgeBackgroundColor = isBadgeHighlighed ? theme.noticeColor : theme.noticeSecondaryColor } private func updateToolbar(with menu: UIMenu) { guard isViewLoaded else { return } self.isToolbarHidden = false self.update(with: theme) // bwi: 4179 var allChatsEditButton = UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) allChatsEditButton.tintColor = ThemeService.shared().theme.tintColor // bwi: #4883 if BWIBuildSettings.shared.enableSpaces { self.toolbar.items = [ spacesButton, UIBarButtonItem.flexibleSpace(), allChatsEditButton ] } else { self.toolbar.items = [ UIBarButtonItem.flexibleSpace(), allChatsEditButton ] } } private func showCreateSpace(parentSpaceId: String?) { let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: self.mainSession, parentSpaceId: parentSpaceId)) let presentable = coordinator.toPresentable() self.present(presentable, animated: true, completion: nil) coordinator.callback = { [weak self] result in guard let self = self else { return } coordinator.toPresentable().dismiss(animated: true) { self.remove(childCoordinator: coordinator) switch result { case .cancel: break case .done(let spaceId): self.switchSpace(withId: spaceId) } } } add(childCoordinator: coordinator) coordinator.start() } private func add(childCoordinator: Coordinator) { self.childCoordinators.append(childCoordinator) } private func remove(childCoordinator: Coordinator) { self.childCoordinators.append(childCoordinator) } private func showSpaceInvite() { guard let session = mainSession, let spaceRoom = dataSource?.currentSpace?.room else { return } let coordinator = ContactsPickerCoordinator(session: session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil) coordinator.delegate = self coordinator.start() add(childCoordinator: coordinator) present(coordinator.toPresentable(), animated: true) } private func showSpaceMembers() { guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } let coordinator = SpaceMembersCoordinator(parameters: SpaceMembersCoordinatorParameters(userSessionsService: UserSessionsService.shared, session: session, spaceId: spaceId)) coordinator.delegate = self let presentable = coordinator.toPresentable() presentable.presentationController?.delegate = self coordinator.start() add(childCoordinator: coordinator) present(presentable, animated: true, completion: nil) } private func showSpaceSettings() { guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId, parentSpaceId: nil)) coordinator.callback = { [weak self] result in guard let self = self else { return } coordinator.toPresentable().dismiss(animated: true) { self.remove(childCoordinator: coordinator) } } let presentable = coordinator.toPresentable() presentable.presentationController?.delegate = self present(presentable, animated: true, completion: nil) coordinator.start() add(childCoordinator: coordinator) } private func showLeaveSpace() { guard let session = mainSession, let spaceSummary = dataSource?.currentSpace?.summary else { return } let name = spaceSummary.displayName ?? VectorL10n.spaceTag let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms) let paramaters = MatrixItemChooserCoordinatorParameters(session: session, title: VectorL10n.leaveSpaceTitle(name), detail: VectorL10n.leaveSpaceMessage(name), selectionHeader: selectionHeader, viewProvider: LeaveSpaceViewProvider(navTitle: nil), itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceSummary.roomId, session: session)) let coordinator = MatrixItemChooserCoordinator(parameters: paramaters) coordinator.toPresentable().presentationController?.delegate = self coordinator.start() add(childCoordinator: coordinator) coordinator.completion = { [weak self] result in // switching to home space self?.switchSpace(withId: nil) coordinator.toPresentable().dismiss(animated: true) { self?.remove(childCoordinator: coordinator) } } present(coordinator.toPresentable(), animated: true) } private func showRoomInviteList() { let invitesViewController = RoomInvitesViewController.instantiate() invitesViewController.userIndicatorStore = self.userIndicatorStore let recentsListService = RecentsListService(withSession: mainSession) let recentsDataSource = RecentsDataSource(matrixSession: mainSession, recentsListService: recentsListService) invitesViewController.displayList(recentsDataSource) self.navigationController?.pushViewController(invitesViewController, animated: true) } } private extension AllChatsViewController { enum Constants { static let spacesButtonMaxCount: UInt = 999 } } // MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate { func spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) { coordinatorBridgePresenter.dismiss(animated: true) { self.spaceSelectorBridgePresenter = nil } } func spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) { coordinatorBridgePresenter.dismiss(animated: true) { self.spaceSelectorBridgePresenter = nil } switchSpace(withId: nil) } func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didSelectSpaceWithId spaceId: String) { coordinatorBridgePresenter.dismiss(animated: true) { self.spaceSelectorBridgePresenter = nil } switchSpace(withId: spaceId) } func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didCreateSpaceWithinSpaceWithId parentSpaceId: String?) { coordinatorBridgePresenter.dismiss(animated: true) { self.spaceSelectorBridgePresenter = nil } self.showCreateSpace(parentSpaceId: parentSpaceId) } } // MARK: - UISearchResultsUpdating extension AllChatsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let searchText = searchController.searchBar.text, !searchText.isEmpty else { self.dataSource?.search(withPatterns: nil) return } self.dataSource?.search(withPatterns: [searchText]) } } // MARK: - UISearchControllerDelegate extension AllChatsViewController: UISearchControllerDelegate { func willPresentSearchController(_ searchController: UISearchController) { // Fix for https://github.com/vector-im/element-ios/issues/6680 self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) } } extension AllChatsViewController { override func searchBar(_ searchBar: UISearchBar, textDidChange: String) { guard let searchText = searchBar.text, !searchText.isEmpty else { self.dataSource?.search(withPatterns: nil) return } self.dataSource?.search(withPatterns: [searchText]) } } // MARK: - UIAdaptivePresentationControllerDelegate extension AllChatsViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // bwi: show matomo consent alert again if showMatomoConsentAlertOnCloseModal { showMatomoConsentAlertOnCloseModal = false bwiPresentMatomoConsentAlert() } guard let coordinator = childCoordinators.last else { return } remove(childCoordinator: coordinator) } } // MARK: - AllChatsEditActionProviderDelegate extension AllChatsViewController: AllChatsEditActionProviderDelegate { func allChatsEditActionProvider(_ actionProvider: AllChatsEditActionProvider, didSelect option: AllChatsEditActionProviderOption) { switch option { case .exploreRooms: joinARoom() case .createRoom: createNewRoom() case .startChat: startChat() case .createSpace: showCreateSpace(parentSpaceId: dataSource?.currentSpace?.spaceId) case .scanPermalink: scanPermalink() } } } // MARK: - AllChatsSpaceActionProviderDelegate extension AllChatsViewController: AllChatsSpaceActionProviderDelegate { func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) { switch option { case .invitePeople: showSpaceInvite() case .spaceMembers: showSpaceMembers() case .spaceSettings: showSpaceSettings() case .leaveSpace: showLeaveSpace() } } } // MARK: - ContactsPickerCoordinatorDelegate extension AllChatsViewController: ContactsPickerCoordinatorDelegate { func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) { } func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) { } func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) { remove(childCoordinator: coordinator) } } // MARK: - SpaceMembersCoordinatorDelegate extension AllChatsViewController: SpaceMembersCoordinatorDelegate { func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) { coordinator.toPresentable().dismiss(animated: true) { self.remove(childCoordinator: coordinator) } } } // MARK: - BannerPresentationProtocol extension AllChatsViewController: BannerPresentationProtocol { func presentBannerView(_ bannerView: UIView, animated: Bool) { self.bannerView = bannerView } func dismissBannerView(animated: Bool) { self.bannerView = nil } } // TODO: The `MasterTabBarViewController` is called from the entire app through the `LegacyAppDelegate`. this part of the code should be moved into `AppCoordinator` // MARK: - SplitViewMasterViewControllerProtocol extension AllChatsViewController: SplitViewMasterViewControllerProtocol { /// Release the current selected item (if any). func releaseSelectedItem() { selectedRoomId = nil selectedEventId = nil selectedRoomSession = nil selectedRoomPreviewData = nil selectedContact = nil } /// Refresh the missed conversations badges on tab bar icon func refreshTabBarBadges() { // Nothing to do here as we don't have tab bar } /// Verify the current device if needed. /// /// - Parameters: /// - session: the matrix session. func presentVerifyCurrentSessionAlertIfNeeded(with session: MXSession) { guard !RiotSettings.shared.hideVerifyThisSessionAlert, !isOnboardingInProgress, presentedViewController == nil, viewIfLoaded?.window != nil else { return } // Force verification if required by the HS configuration guard !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired else { MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertIfNeededWithSession: Force verification of the device") AppDelegate.theDelegate().presentCompleteSecurity(for: session) return } presentVerifyCurrentSessionAlert(with: session) } /// Verify others device if needed. /// /// - Parameters: /// - session: the matrix session. func presentReviewUnverifiedSessionsAlertIfNeeded(with session: MXSession) { guard BuildSettings.showUnverifiedSessionsAlert, !reviewSessionAlertSnoozeController.isSnoozed(), presentedViewController == nil, viewIfLoaded?.window != nil else { return } if let userId = mainSession.myUserId, let crypto = mainSession.crypto { let devices = crypto.devices(forUser: userId).values let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) if userHasOneUnverifiedDevice { presentReviewUnverifiedSessionsAlert(with: session) } } } func showOnboardingFlow() { MXLog.debug("[AllChatsViewController] showOnboardingFlow") self.showOnboardingFlowAndResetSessionFlags(true) } /// Display the onboarding flow configured to log back into a soft logout session. /// /// - Parameters: /// - credentials: the credentials of the soft logout session. func showSoftLogoutOnboardingFlow(with credentials: MXCredentials?) { // This method can be called after the user chooses to clear their data as the MXSession // is opened to call logout from. So we only set the credentials when authentication isn't // in progress to prevent a second soft logout screen being shown. guard self.onboardingCoordinatorBridgePresenter == nil && !self.isOnboardingCoordinatorPreparing else { return } MXLog.debug("[AllChatsViewController] showAuthenticationScreenAfterSoftLogout") AuthenticationService.shared.softLogoutCredentials = credentials self.showOnboardingFlowAndResetSessionFlags(false) } /// Open the room with the provided identifier in a specific matrix session. /// /// - Parameters: /// - parameters: the presentation parameters that contains room information plus display information. /// - completion: the block to execute at the end of the operation. func selectRoom(with parameters: RoomNavigationParameters, completion: @escaping () -> Void) { releaseSelectedItem() selectedRoomId = parameters.roomId selectedEventId = parameters.eventId selectedRoomSession = parameters.mxSession allChatsDelegate?.allChatsViewController(self, didSelectRoomWithParameters: parameters, completion: completion) refreshSelectedControllerSelectedCellIfNeeded() } /// Open the RoomViewController to display the preview of a room that is unknown for the user. /// This room can come from an email invitation link or a simple link to a room. /// - Parameters: /// - parameters: the presentation parameters that contains room preview information plus display information. /// - completion: the block to execute at the end of the operation. func selectRoomPreview(with parameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) { releaseSelectedItem() let roomPreviewData = parameters.previewData selectedRoomPreviewData = roomPreviewData selectedRoomId = roomPreviewData.roomId selectedRoomSession = roomPreviewData.mxSession allChatsDelegate?.allChatsViewController(self, didSelectRoomPreviewWithParameters: parameters, completion: completion) refreshSelectedControllerSelectedCellIfNeeded() } /// Open a ContactDetailsViewController to display the information of the provided contact. func select(_ contact: MXKContact) { let presentationParameters = ScreenPresentationParameters(restoreInitialDisplay: true, stackAboveVisibleViews: false) select(contact, with: presentationParameters) } /// Open a ContactDetailsViewController to display the information of the provided contact. func select(_ contact: MXKContact, with presentationParameters: ScreenPresentationParameters) { releaseSelectedItem() selectedContact = contact allChatsDelegate?.allChatsViewController(self, didSelectContact: contact, with: presentationParameters) refreshSelectedControllerSelectedCellIfNeeded() } /// The current number of rooms with missed notifications, including the invites. func missedDiscussionsCount() -> UInt { guard let session = mxSessions as? [MXSession] else { return 0 } return session.reduce(0) { $0 + $1.vc_missedDiscussionsCount() } } /// The current number of rooms with unread highlighted messages. func missedHighlightDiscussionsCount() -> UInt { guard let session = mxSessions as? [MXSession] else { return 0 } return session.reduce(0) { $0 + $1.missedHighlightDiscussionsCount() } } /// Emulated `UItabBarViewController.selectedViewController` member var selectedViewController: UIViewController? { return self } var tabBar: UITabBar? { return nil } func bwiOnUnlockedByPin() { // bwi specific } // MARK: - Private private func presentVerifyCurrentSessionAlert(with session: MXSession) { MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertWithSession") let title: String let message: String if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true { title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage } else { title = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle message = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage } let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertValidateAction, style: .default, handler: { action in AppDelegate.theDelegate().presentCompleteSecurity(for: session) })) alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel)) alert.addAction(UIAlertAction(title: VectorL10n.doNotAskAgain, style: .destructive, handler: { action in RiotSettings.shared.hideVerifyThisSessionAlert = true })) self.present(alert, animated: true) } private func presentReviewUnverifiedSessionsAlert(with session: MXSession) { MXLog.debug("[AllChatsViewController] presentReviewUnverifiedSessionsAlert") let alert = UIAlertController(title: VectorL10n.keyVerificationAlertTitle, message: VectorL10n.keyVerificationAlertBody, preferredStyle: .alert) alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction, style: .default, handler: { action in self.showSettingsSecurityScreen(with: session) })) alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel, handler: { [weak self] _ in self?.reviewSessionAlertSnoozeController.snooze() })) present(alert, animated: true) } private func showSettingsSecurityScreen(with session: MXSession) { guard let settingsViewController = SettingsViewController.instantiate() else { MXLog.warning("[AllChatsViewController] showSettingsSecurityScreen: cannot instantiate SettingsViewController") return } guard let securityViewController = SecurityViewController.instantiate(withMatrixSession: session) else { MXLog.warning("[AllChatsViewController] showSettingsSecurityScreen: cannot instantiate SecurityViewController") return } settingsViewController.loadViewIfNeeded() AppDelegate.theDelegate().restoreInitialDisplay { if RiotSettings.shared.enableNewSessionManager || BWIBuildSettings.shared.enableNewSessionManagerByDefault { self.navigationController?.viewControllers = [self, settingsViewController] settingsViewController.showUserSessionsFlow() } else { self.navigationController?.viewControllers = [self, settingsViewController, securityViewController] } } } private func showOnboardingFlowAndResetSessionFlags(_ resetSessionFlags: Bool) { // Check whether an authentication screen is not already shown or preparing guard self.onboardingCoordinatorBridgePresenter == nil && !self.isOnboardingCoordinatorPreparing else { return } self.isOnboardingCoordinatorPreparing = true self.isOnboardingInProgress = true if resetSessionFlags { resetReviewSessionsFlags() } AppDelegate.theDelegate().restoreInitialDisplay { self.presentOnboardingFlow() } } private func resetReviewSessionsFlags() { RiotSettings.shared.hideVerifyThisSessionAlert = false } private func presentOnboardingFlow() { MXLog.debug("[AllChatsViewController] presentOnboardingFlow") let onboardingCoordinatorBridgePresenter = OnboardingCoordinatorBridgePresenter() onboardingCoordinatorBridgePresenter.completion = { [weak self] in guard let self = self else { return } self.onboardingCoordinatorBridgePresenter?.dismiss(animated: true, completion: { self.onboardingCoordinatorBridgePresenter = nil }) self.isOnboardingInProgress = false // Must be set before calling didCompleteAuthentication self.allChatsDelegate?.allChatsViewControllerDidCompleteAuthentication(self) } onboardingCoordinatorBridgePresenter.present(from: self, animated: true) self.onboardingCoordinatorBridgePresenter = onboardingCoordinatorBridgePresenter self.isOnboardingCoordinatorPreparing = false } private func refreshSelectedControllerSelectedCellIfNeeded() { guard splitViewController != nil else { return } // Refresh selected cell without scrolling the selected cell (We suppose it's visible here) self.refreshCurrentSelectedCell(false) } } private extension MXSpaceService { var hasSpaceInvite: Bool { spaceSummaries.contains(where: { $0.isJoined == false }) } var missedNotificationsCount: UInt { let notificationState = notificationCounter.homeNotificationState let groupNotifications = notificationState.groupMissedDiscussionsCount let directNotifications = notificationState.directMissedDiscussionsCount // `notificationState.allCount` returns twice the messages for favourite rooms. Fixing it here. return groupNotifications + directNotifications } var hasHighlightNotification: Bool { notificationCounter.homeNotificationState.allHighlightCount > 0 } }