diff --git a/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift b/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift index 8be85ff9c..f595dfee9 100644 --- a/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift +++ b/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift @@ -16,7 +16,7 @@ import Foundation -class UserIndicatorPresenterSpy: UserIndicatorPresentable { +class UserIndicatorPresenterSpy: UserIndicatorViewPresentable { var intel = [String]() func present() { diff --git a/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift b/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift index 00a90913e..3a4a663f0 100644 --- a/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift @@ -17,9 +17,10 @@ import Foundation /// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen. -public protocol UserIndicatorPresentable { +public protocol UserIndicatorViewPresentable { /// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`) func present() /// Called when the `UserIndicator` is manually cancelled or completed func dismiss() } + diff --git a/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift b/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift index b66014c0f..a2ba49fcf 100644 --- a/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift @@ -28,8 +28,11 @@ public class UserIndicatorQueue { } } - public static let shared = UserIndicatorQueue() - private var queue = [Weak]() + private var queue: [Weak] + + public init() { + queue = [] + } /// Add a new indicator to the queue by providing a request. /// diff --git a/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift b/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift index 83e882c0e..97611eeea 100644 --- a/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift @@ -18,10 +18,10 @@ import Foundation /// A request used to create an underlying `UserIndicator`, allowing clients to only specify the visual aspects of an indicator. public struct UserIndicatorRequest { - internal let presenter: UserIndicatorPresentable + internal let presenter: UserIndicatorViewPresentable internal let dismissal: UserIndicatorDismissal - public init(presenter: UserIndicatorPresentable, dismissal: UserIndicatorDismissal) { + public init(presenter: UserIndicatorViewPresentable, dismissal: UserIndicatorDismissal) { self.presenter = presenter self.dismissal = dismissal } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 93ae25fe5..3413a90ad 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -297,19 +297,7 @@ fileprivate class AppNavigator: AppNavigatorProtocol { return SideMenuPresenter(sideMenuCoordinator: sideMenuCoordinator) }() - - private var appNavigationVC: UINavigationController { - guard - let splitVC = appCoordinator.splitViewCoordinator?.toPresentable() as? UISplitViewController, - // Picking out the first view controller currently works only on iPhones, not iPads - let navigationVC = splitVC.viewControllers.first as? UINavigationController - else { - MXLog.error("[AppNavigator] Missing root split view controller") - return UINavigationController() - } - return navigationVC - } - + // MARK: - Setup init(appCoordinator: AppCoordinator) { @@ -321,41 +309,4 @@ fileprivate class AppNavigator: AppNavigatorProtocol { func navigate(to destination: AppNavigatorDestination) { self.appCoordinator.navigate(to: destination) } - - func addUserIndicator(_ type: AppUserIndicatorType) -> UserIndicator { - let request = userIndicatorRequest(for: type) - return UserIndicatorQueue.shared.add(request) - } - - // MARK: - Private - - private func userIndicatorRequest(for type: AppUserIndicatorType) -> UserIndicatorRequest { - switch type { - case let .loading(label): - let presenter = ToastUserIndicatorPresenter( - viewState: .init( - style: .loading, - label: label - ), - navigationController: appNavigationVC - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .manual - ) - case let .success(label): - let presenter = ToastUserIndicatorPresenter( - viewState: .init( - style: .success, - label: label - ), - navigationController: appNavigationVC - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .timeout(1.5) - ) - } - } } - diff --git a/Riot/Modules/Application/AppNavigator.swift b/Riot/Modules/Application/AppNavigator.swift index dd0eff1eb..13404e62d 100644 --- a/Riot/Modules/Application/AppNavigator.swift +++ b/Riot/Modules/Application/AppNavigator.swift @@ -17,15 +17,6 @@ import Foundation import CommonKit -/// Type of indicator to be shown in the app navigator -enum AppUserIndicatorType { - /// Loading toast with custom label - case loading(String) - - /// Success toast with custom label - case success(String) -} - /// AppNavigatorProtocol abstract a navigator at app level. /// It enables to perform the navigation within the global app scope (open the side menu, open a room and so on) /// Note: Presentation of the pattern here https://www.swiftbysundell.com/articles/navigation-in-swift/#where-to-navigator @@ -36,12 +27,4 @@ protocol AppNavigatorProtocol { /// Navigate to a destination screen or a state /// Do not use protocol with associatedtype for the moment like presented here https://www.swiftbysundell.com/articles/navigation-in-swift/#where-to-navigator use a separate enum func navigate(to destination: AppNavigatorDestination) - - /// Add new indicator, such as loading spinner or a success message, to an app-wide queue of other indicators - /// - /// If the queue is empty, the indicator will be displayed immediately, otherwise it will be pending - /// until the previously added indicator have completed / been cancelled. - /// - /// To remove an indicator, cancel or deallocate the returned `UserIndicator` - func addUserIndicator(_ type: AppUserIndicatorType) -> UserIndicator } diff --git a/Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift deleted file mode 100644 index 245b5e323..000000000 --- a/Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright 2021 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. -// - -import Foundation -import UIKit -import MatrixSDK -import CommonKit - -/// Presenter which displays loading spinners using app-wide `AppNavigator`, thus displaying them in a unified way, -/// and `UserIndicatorCenter`/`UserIndicator`, which ensures that only one indicator is shown at a given time. -/// -/// Note: clients can skip using `AppUserIndicatorPresenter` and instead coordinate with `AppNavigatorProtocol` directly. -/// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers -/// written in objective-c. -@objc final class AppUserIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { - private let appNavigator: AppNavigatorProtocol - private var loadingIndicator: UserIndicator? - private var otherIndicators = [UserIndicator]() - - init(appNavigator: AppNavigatorProtocol) { - self.appNavigator = appNavigator - } - - @objc func presentActivityIndicator() { - presentActivityIndicator(label: VectorL10n.homeSyncing) - } - - @objc func presentActivityIndicator(label: String) { - guard loadingIndicator == nil || loadingIndicator?.state == .completed else { - // The app is very liberal with calling `presentActivityIndicator` (often not matched by corresponding `removeCurrentActivityIndicator`), - // so there is no reason to keep adding new indiciators if there is one already showing. - return - } - - loadingIndicator = appNavigator.addUserIndicator(.loading(label)) - } - - @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { - loadingIndicator = nil - } - - func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { - MXLog.error("[AppUserIndicatorPresenter] Shared indicator presenter does not support presenting from custom views") - } - - @objc func presentSuccess(label: String) { - appNavigator.addUserIndicator(.success(label)).store(in: &otherIndicators) - } -} diff --git a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift deleted file mode 100644 index 610f56f13..000000000 --- a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright 2021 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. -// - -import Foundation -import CommonKit -import UIKit - -/// Presenter which displays fullscreen loading spinners, and conforming to legacy `ActivityIndicatorPresenterType`, -/// but interally wrapping an `UserIndicatorPresenter` which is used in conjuction with `UserIndicator` and `UserIndicatorQueue`. -/// -/// Note: clients can skip using `FullscreenActivityIndicatorPresenter` and instead coordinate with `AppNavigatorProtocol` directly. -/// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers -/// written in objective-c. -@objc final class FullscreenActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { - private let label: String - private weak var viewController: UIViewController? - private var indicator: UserIndicator? - - init(label: String, viewController: UIViewController) { - self.label = label - self.viewController = viewController - } - - func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { - guard let viewController = viewController else { - return - } - - let request = UserIndicatorRequest( - presenter: FullscreenLoadingIndicatorPresenter(label: label, viewController: viewController), - dismissal: .manual - ) - - indicator = UserIndicatorQueue.shared.add(request) - } - - @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { - indicator?.cancel() - } -} diff --git a/Riot/Modules/Common/ActivityIndicator/UserIndicatorPresenterWrapper.swift b/Riot/Modules/Common/ActivityIndicator/UserIndicatorPresenterWrapper.swift new file mode 100644 index 000000000..f7faa03f6 --- /dev/null +++ b/Riot/Modules/Common/ActivityIndicator/UserIndicatorPresenterWrapper.swift @@ -0,0 +1,54 @@ +// +// Copyright 2021 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. +// + +import Foundation +import CommonKit + +/// A convenience objc-compatible wrapper around `UserIndicatorTypePresenterProtocol`. +/// +/// This class wraps swift-only protocol by exposing multiple methods instead of accepting struct types +/// and it keeps a track of `UserIndicator`s instead of returning them to the caller. +@objc final class UserIndicatorPresenterWrapper: NSObject { + private let presenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + private var otherIndicators = [UserIndicator]() + + init(presenter: UserIndicatorTypePresenterProtocol) { + self.presenter = presenter + } + + @objc func presentActivityIndicator() { + presentActivityIndicator(label: VectorL10n.homeSyncing) + } + + @objc func presentActivityIndicator(label: String) { + guard loadingIndicator == nil else { + // The app is very liberal with calling `presentActivityIndicator` (often not matched by corresponding `removeCurrentActivityIndicator`), + // so there is no reason to keep adding new indiciators if there is one already showing. + return + } + + loadingIndicator = presenter.present(.loading(label: label, isInteractionBlocking: false)) + } + + @objc func dismissActivityIndicator() { + loadingIndicator = nil + } + + @objc func presentSuccess(label: String) { + presenter.present(.success(label: label)).store(in: &otherIndicators) + } +} diff --git a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift deleted file mode 100644 index 504b4600c..000000000 --- a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright 2021 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. -// - -import Foundation -import UIKit -import CommonKit -import MatrixSDK - -/// A `UserIndicatorPresentable` responsible for showing / hiding a toast view for loading spinners or success messages. -/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. -class ToastUserIndicatorPresenter: UserIndicatorPresentable { - private let viewState: ToastViewState - private weak var navigationController: UINavigationController? - private weak var view: UIView? - - init(viewState: ToastViewState, navigationController: UINavigationController) { - self.viewState = viewState - self.navigationController = navigationController - } - - func present() { - guard let navigationController = navigationController else { - return - } - - let view = RoundedToastView(viewState: viewState) - view.update(theme: ThemeService.shared().theme) - self.view = view - - view.translatesAutoresizingMaskIntoConstraints = false - navigationController.view.addSubview(view) - NSLayoutConstraint.activate([ - view.centerXAnchor.constraint(equalTo: navigationController.view.centerXAnchor), - view.topAnchor.constraint(equalTo: navigationController.navigationBar.safeAreaLayoutGuide.bottomAnchor) - ]) - - view.alpha = 0 - view.transform = .init(translationX: 0, y: 5) - UIView.animate(withDuration: 0.2) { - view.alpha = 1 - view.transform = .identity - } - } - - func dismiss() { - guard let view = view, view.superview != nil else { - return - } - - // If `present` and `dismiss` are called right after each other without delay, - // the view does not correctly pick up `currentState` of alpha. Dispatching onto - // the main queue skips a few run loops, giving the system time to render - // current state. - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - view.transform = .init(translationX: 0, y: -5) - } completion: { _ in - view.removeFromSuperview() - } - } - } -} diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index 9c29a28f8..e46027035 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -19,7 +19,7 @@ @class RootTabEmptyView; @class AnalyticsScreenTimer; -@class AppUserIndicatorPresenter; +@class UserIndicatorPresenterWrapper; /** Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. @@ -100,7 +100,7 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; /** Presenter for displaying app-wide user indicators. If not set, the view controller will use legacy activity indicators */ -@property (nonatomic, strong) AppUserIndicatorPresenter *userIndicatorPresenter; +@property (nonatomic, strong) UserIndicatorPresenterWrapper *indicatorPresenter; /** Return the sticky header for the specified section of the table view diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 2f9893d32..694773f92 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1298,7 +1298,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { typeof(self) self = weakSelf; [self stopActivityIndicator]; - [self.userIndicatorPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]]; + [self.indicatorPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]]; // Force table refresh [self cancelEditionMode:YES]; } @@ -2413,28 +2413,28 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro #pragma mark - Activity Indicator - (BOOL)providesCustomActivityIndicator { - return self.userIndicatorPresenter != nil; + return self.indicatorPresenter != nil; } - (void)startActivityIndicatorWithLabel:(NSString *)label { - if (self.userIndicatorPresenter) { - [self.userIndicatorPresenter presentActivityIndicatorWithLabel:label]; + if (self.indicatorPresenter) { + [self.indicatorPresenter presentActivityIndicatorWithLabel:label]; } else { [super startActivityIndicator]; } } - (void)startActivityIndicator { - if (self.userIndicatorPresenter) { - [self.userIndicatorPresenter presentActivityIndicator]; + if (self.indicatorPresenter) { + [self.indicatorPresenter presentActivityIndicator]; } else { [super startActivityIndicator]; } } - (void)stopActivityIndicator { - if (self.userIndicatorPresenter) { - [self.userIndicatorPresenter removeCurrentActivityIndicatorWithAnimated:YES completion:nil]; + if (self.indicatorPresenter) { + [self.indicatorPresenter dismissActivityIndicator]; } else { [super stopActivityIndicator]; } diff --git a/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift new file mode 100644 index 000000000..872a7191f --- /dev/null +++ b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift @@ -0,0 +1,126 @@ +// +// Copyright 2021 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. +// + +import Foundation +import CommonKit +import MatrixSDK +import UIKit + +/// A set of user interactors commonly used across the app +enum UserIndicatorType { + case loading(label: String, isInteractionBlocking: Bool) + case success(label: String) +} + +/// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator` +/// and adding it to its `UserIndicatorQueue` +protocol UserIndicatorTypePresenterProtocol { + /// Present a new type of user indicator, such as loading spinner or success message. + /// + /// The presenter will internally convert the type into a `UserIndicator` and add it to its internal queue + /// of other indicators. + /// + /// If the queue is empty, the indicator will be displayed immediately, otherwise it will be pending + /// until the previously added indicators have completed / been cancelled. + /// + /// To remove an indicator, `cancel` or deallocate the returned `UserIndicator` + func present(_ type: UserIndicatorType) -> UserIndicator + + /// The queue of user indicators owned by the presenter + /// + /// Clients can access the queue to add custom `UserIndicatorRequest`s + /// above and beyond those defined by `UserIndicatorType` + var queue: UserIndicatorQueue { get } +} + +class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol { + private weak var viewController: UIViewController? + + // In the existing app architecture it is often view controllers which instantiate + // various presenters (errors, alerts ... ) and present on self. Since the presenting view controller + // needs to be passed on init, it must be declared as weak, otherwise a retain cycle would occur. + private var presentingViewController: UIViewController { + guard let viewController = viewController else { + MXLog.error("[UserIndicatorTypePresenter]: Presenting view controller is not available") + return UIViewController() + } + return viewController + } + + let queue: UserIndicatorQueue + + init(presentingViewController: UIViewController) { + self.viewController = presentingViewController + self.queue = UserIndicatorQueue() + } + + func present(_ type: UserIndicatorType) -> UserIndicator { + let request = userIndicatorRequest(for: type) + return queue.add(request) + } + + private func userIndicatorRequest(for type: UserIndicatorType) -> UserIndicatorRequest { + switch type { + case .loading(let label, let isInteractionBlocking): + if isInteractionBlocking { + return fullScreenLoadingRequest(label: label) + } else { + return loadingRequest(label: label) + } + case .success(let label): + return successRequest(label: label) + } + } + + private func loadingRequest(label: String) -> UserIndicatorRequest { + let presenter = ToastViewPresenter( + viewState: .init( + style: .loading, + label: label + ), + presentingViewController: presentingViewController + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .manual + ) + } + + private func fullScreenLoadingRequest(label: String) -> UserIndicatorRequest { + let presenter = FullscreenLoadingViewPresenter( + label: label, + presentingViewController: presentingViewController + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .manual + ) + } + + private func successRequest(label: String) -> UserIndicatorRequest { + let presenter = ToastViewPresenter( + viewState: .init( + style: .success, + label: label + ), + presentingViewController: presentingViewController + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .timeout(1.5) + ) + } +} diff --git a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift b/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift similarity index 71% rename from Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift rename to Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift index e5488a19a..5532bcb2a 100644 --- a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift +++ b/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift @@ -18,16 +18,17 @@ import Foundation import CommonKit import UIKit -/// A `UserIndicatorPresentable` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. +/// A presenter responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. /// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. -class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { +class FullscreenLoadingViewPresenter: UserIndicatorViewPresentable { private let label: String private weak var viewController: UIViewController? private weak var view: UIView? + private var animator: UIViewPropertyAnimator? - init(label: String, viewController: UIViewController) { + init(label: String, presentingViewController: UIViewController) { self.label = label - self.viewController = viewController + self.viewController = presentingViewController } func present() { @@ -54,9 +55,10 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { ]) view.alpha = 0 - UIView.animate(withDuration: 0.2) { + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { view.alpha = 1 } + animator?.startAnimation() } func dismiss() { @@ -64,16 +66,13 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { return } - // If `present` and `dismiss` are called right after each other without delay, - // the view does not correctly pick up `currentState` of alpha. Dispatching onto - // the main queue skips a few run loops, giving the system time to render - // current state. - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - } completion: { _ in - view.removeFromSuperview() - } + animator?.stopAnimation(true) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { + view.alpha = 0 } + animator?.addCompletion { _ in + view.removeFromSuperview() + } + animator?.startAnimation() } } diff --git a/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift new file mode 100644 index 000000000..b56e511d2 --- /dev/null +++ b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift @@ -0,0 +1,97 @@ +// +// Copyright 2021 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. +// + +import Foundation +import UIKit +import CommonKit +import MatrixSDK + +/// A presenter responsible for showing / hiding a toast view for loading spinners or success messages. +/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. +class ToastViewPresenter: UserIndicatorViewPresentable { + struct Constants { + static let navigationBarPatting = CGFloat(10) + } + + private let viewState: ToastViewState + private weak var viewController: UIViewController? + private weak var view: UIView? + private var animator: UIViewPropertyAnimator? + + init(viewState: ToastViewState, presentingViewController: UIViewController) { + self.viewState = viewState + self.viewController = presentingViewController + } + + func present() { + guard let viewController = viewController else { + return + } + + let view = RoundedToastView(viewState: viewState) + view.update(theme: ThemeService.shared().theme) + self.view = view + + view.translatesAutoresizingMaskIntoConstraints = false + if let navigation = viewController.topNavigationController { + navigation.view.addSubview(view) + NSLayoutConstraint.activate([ + view.centerXAnchor.constraint(equalTo: navigation.view.centerXAnchor), + view.topAnchor.constraint(equalTo: navigation.navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.navigationBarPatting) + ]) + } else { + viewController.view.addSubview(view) + NSLayoutConstraint.activate([ + view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), + view.topAnchor.constraint(equalTo: viewController.view.topAnchor) + ]) + } + + view.alpha = 0 + view.transform = .init(translationX: 0, y: 5) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { + view.alpha = 1 + view.transform = .identity + } + animator?.startAnimation() + } + + func dismiss() { + guard let view = view, view.superview != nil else { + return + } + + animator?.stopAnimation(true) + animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { + view.alpha = 0 + view.transform = .init(translationX: 0, y: -5) + } + animator?.addCompletion { _ in + view.removeFromSuperview() + } + animator?.startAnimation() + } +} + +private extension UIViewController { + var topNavigationController: UINavigationController? { + var controller: UINavigationController? = self as? UINavigationController ?? navigationController + while controller?.navigationController != nil { + controller = controller?.navigationController + } + return controller + } +} diff --git a/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.h b/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.h index 400b9cc3b..468004818 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.h +++ b/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.h @@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN @interface MXKActivityHandlingViewController : UIViewController +/// A subclass can override this method to block `stopActivityIndicator` if there are still activities in progress +- (BOOL)canStopActivityIndicator; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.m index 97cce741e..8fbdac314 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKActivityHandlingViewController.m @@ -83,9 +83,15 @@ } } +- (BOOL)canStopActivityIndicator { + return YES; +} + - (void)stopActivityIndicator { - [activityIndicator stopAnimating]; + if ([self canStopActivityIndicator]) { + [activityIndicator stopAnimating]; + } } @end diff --git a/Riot/Modules/MatrixKit/Controllers/MXKViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKViewController.m index a4e559b4c..07b0a2044 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKViewController.m @@ -492,21 +492,16 @@ const CGFloat MXKViewControllerMaxExternalKeyboardHeight = 80; #pragma mark - Activity indicator -- (void)stopActivityIndicator -{ +- (BOOL)canStopActivityIndicator { // Check whether all conditions are satisfied before stopping loading wheel - BOOL isActivityInProgress = NO; for (MXSession *mxSession in mxSessionArray) { if (mxSession.shouldShowActivityIndicator) { - isActivityInProgress = YES; + return NO; } } - if (!isActivityInProgress) - { - [super stopActivityIndicator]; - } + return [super canStopActivityIndicator]; } #pragma mark - Shake handling diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 65441804f..44cfd4150 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -1772,23 +1772,21 @@ #pragma mark - activity indicator -- (void)stopActivityIndicator -{ +- (BOOL)canStopActivityIndicator { // Keep the loading wheel displayed while we are joining the room if (joinRoomRequest) { - return; + return NO; } // Check internal processes before stopping the loading wheel if (isPaginationInProgress || isInputToolbarProcessing) { // Keep activity indicator running - return; + return NO; } - // Leave super decide - [super stopActivityIndicator]; + return [super canStopActivityIndicator]; } #pragma mark - Pagination diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 6bde3450c..ad6e7b4f2 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -18,6 +18,7 @@ import Foundation import UIKit +import CommonKit final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { @@ -29,6 +30,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let roomViewController: RoomViewController private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? + private var loadingIndicator: UserIndicator? private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -149,7 +151,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private func loadRoom(withId roomId: String, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID - self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + startLoading() let roomDataSourceManager: MXKRoomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -160,7 +162,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { return } - self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + self.stopLoading() if let roomDataSource = roomDataSource { self.roomViewController.displayRoom(roomDataSource) @@ -173,7 +175,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private func loadRoom(withId roomId: String, andEventId eventId: String, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID - self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + startLoading() // Open the room on the requested event RoomDataSource.load(withRoomId: roomId, @@ -185,7 +187,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { return } - self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + self.stopLoading() guard let roomDataSource = dataSource as? RoomDataSource else { return @@ -204,7 +206,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private func loadRoom(withId roomId: String, andThreadId threadId: String, eventId: String?, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID - self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + startLoading() // Open the thread on the requested event ThreadDataSource.load(withRoomId: roomId, @@ -216,7 +218,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { return } - self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + self.stopLoading() guard let threadDataSource = dataSource as? ThreadDataSource else { return @@ -312,6 +314,19 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter?.present(coordinator, animated: true) coordinator.start() } + + private func startLoading() { + if let presenter = parameters.userIndicatorPresenter { + loadingIndicator = presenter.present(.loading(label: VectorL10n.homeSyncing, isInteractionBlocking: false)) + } else { + activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: true) + } + } + + private func stopLoading() { + loadingIndicator = nil + activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + } } // MARK: - RoomIdentifiable @@ -421,4 +436,16 @@ extension RoomCoordinator: RoomViewControllerDelegate { func roomViewController(_ roomViewController: RoomViewController, didRequestEditForPollWithStart startEvent: MXEvent) { startEditPollCoordinator(startEvent: startEvent) } + + func roomViewControllerCanDelegateUserIndicators(_ roomViewController: RoomViewController) -> Bool { + return BuildSettings.useAppUserIndicators + } + + func roomViewControllerDidStartLoading(_ roomViewController: RoomViewController) { + startLoading() + } + + func roomViewControllerDidStopLoading(_ roomViewController: RoomViewController) { + stopLoading() + } } diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift index 13c47a039..ae31416e8 100644 --- a/Riot/Modules/Room/RoomCoordinatorParameters.swift +++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift @@ -28,6 +28,9 @@ struct RoomCoordinatorParameters { /// `navigationRouter` property takes priority on `navigationRouterStore` let navigationRouterStore: NavigationRouterStoreProtocol? + /// Presenter for displaying loading indicators, success messages and other user indicators + let userIndicatorPresenter: UserIndicatorTypePresenterProtocol? + /// The matrix session in which the room should be available. let session: MXSession @@ -50,6 +53,7 @@ struct RoomCoordinatorParameters { private init(navigationRouter: NavigationRouterType?, navigationRouterStore: NavigationRouterStoreProtocol?, + userIndicatorPresenter: UserIndicatorTypePresenterProtocol?, session: MXSession, roomId: String, eventId: String?, @@ -58,6 +62,7 @@ struct RoomCoordinatorParameters { previewData: RoomPreviewData?) { self.navigationRouter = navigationRouter self.navigationRouterStore = navigationRouterStore + self.userIndicatorPresenter = userIndicatorPresenter self.session = session self.roomId = roomId self.eventId = eventId @@ -69,6 +74,7 @@ struct RoomCoordinatorParameters { /// Init to present a joined room init(navigationRouter: NavigationRouterType? = nil, navigationRouterStore: NavigationRouterStoreProtocol? = nil, + userIndicatorPresenter: UserIndicatorTypePresenterProtocol? = nil, session: MXSession, roomId: String, eventId: String? = nil, @@ -77,6 +83,7 @@ struct RoomCoordinatorParameters { self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, + userIndicatorPresenter: userIndicatorPresenter, session: session, roomId: roomId, eventId: eventId, @@ -92,6 +99,7 @@ struct RoomCoordinatorParameters { self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, + userIndicatorPresenter: nil, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 4f9cd8c47..4171f86fc 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -41,6 +41,8 @@ final class RoomInfoListViewController: UIViewController { private var theme: Theme! private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenterType! + private var indicatorPresenter: UserIndicatorTypePresenterProtocol! + private var loadingIndicator: UserIndicator? private var isRoomDirect: Bool = false private var screenTimer = AnalyticsScreenTimer(screen: .roomDetails) @@ -116,14 +118,10 @@ final class RoomInfoListViewController: UIViewController { // Do any additional setup after loading the view. self.setupViews() - if BuildSettings.useAppUserIndicators { - self.activityPresenter = FullscreenActivityIndicatorPresenter( - label: VectorL10n.roomParticipantsLeaveProcessing, - viewController: self - ) - } else { - self.activityPresenter = ActivityIndicatorPresenter() - } + + self.activityPresenter = ActivityIndicatorPresenter() + self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: self) + self.errorPresenter = MXKErrorAlertPresentation() self.registerThemeServiceDidChangeThemeNotification() @@ -152,7 +150,7 @@ final class RoomInfoListViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) screenTimer.stop() - activityPresenter.removeCurrentActivityIndicator(animated: animated) + stopLoading() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -272,19 +270,36 @@ final class RoomInfoListViewController: UIViewController { } private func renderLoading() { - self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + if BuildSettings.useAppUserIndicators { + loadingIndicator = indicatorPresenter.present( + .loading( + label: VectorL10n.roomParticipantsLeaveProcessing, + isInteractionBlocking: true + ) + ) + } else { + activityPresenter.presentActivityIndicator(on: self.view, animated: true) + } } private func renderLoaded(viewData: RoomInfoListViewData) { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) + stopLoading() self.updateSections(with: viewData) } private func render(error: Error) { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) + stopLoading() self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) } + private func stopLoading() { + if BuildSettings.useAppUserIndicators { + loadingIndicator?.cancel() + } else { + activityPresenter.removeCurrentActivityIndicator(animated: true) + } + } + // MARK: - Actions @objc private func closeButtonTapped(_ sender: Any) { diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index d55354979..1513bafb5 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -234,6 +234,30 @@ canEditPollWithEventIdentifier:(NSString *)eventIdentifier; - (void)roomViewController:(RoomViewController *)roomViewController didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; +/** + Checks whether the delegate supports handling of activity indicators + + Note: This is a transition API whilst `RoomViewController` contains legacy activity indicators + as well as using a newer user interaction presenters. + */ +- (BOOL)roomViewControllerCanDelegateUserIndicators:(RoomViewController *)roomViewController; + +/** + Indicate to the delegate that loading should start + + Note: Only called if the controller can delegate user indicators rather than managing + loading indicators internally + */ +- (void)roomViewControllerDidStartLoading:(RoomViewController *)roomViewController; + +/** + Indicate to the delegate that loading should stop + + Note: Only called if the controller can delegate user indicators rather than managing + loading indicators internally + */ +- (void)roomViewControllerDidStopLoading:(RoomViewController *)roomViewController; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 0bad580dc..2eb4ff370 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -570,6 +570,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; isAppeared = NO; [VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices]; + [self stopActivityIndicator]; } - (void)viewDidAppear:(BOOL)animated @@ -931,6 +932,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.updateRoomReadMarker = NO; } +- (BOOL)providesCustomActivityIndicator { + return [self.delegate roomViewControllerCanDelegateUserIndicators:self]; +} + +- (void)startActivityIndicator { + if ([self providesCustomActivityIndicator]) { + [self.delegate roomViewControllerDidStartLoading:self]; + } else { + [super startActivityIndicator]; + } +} + - (void)stopActivityIndicator { if (notificationTaskProfile) @@ -939,8 +952,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [MXSDKOptions.sharedInstance.profiler stopMeasuringTaskWithProfile:notificationTaskProfile]; notificationTaskProfile = nil; } - - [super stopActivityIndicator]; + if ([self providesCustomActivityIndicator]) { + if ([self canStopActivityIndicator]) { + [self.delegate roomViewControllerDidStopLoading:self]; + } + } else { + [super stopActivityIndicator]; + } } - (void)displayRoom:(MXKRoomDataSource *)dataSource diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 2157cbd31..3f4501a28 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -48,7 +48,7 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { private weak var masterPresentable: SplitViewMasterPresentable? private var detailNavigationController: UINavigationController? - private var detailNavigationRouter: NavigationRouterType? + private(set) var detailNavigationRouter: NavigationRouterType? private var selectedNavigationRouter: NavigationRouterType? { return self.masterPresentable?.selectedNavigationRouter @@ -324,7 +324,6 @@ extension SplitViewCoordinator: TabBarCoordinatorDelegate { // MARK: - SplitViewMasterPresentableDelegate extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { - var detailModules: [Presentable] { return self.detailNavigationRouter?.modules ?? [] } diff --git a/Riot/Modules/SplitView/SplitViewPresentable.swift b/Riot/Modules/SplitView/SplitViewPresentable.swift index 4e63663f1..9af43c0ed 100644 --- a/Riot/Modules/SplitView/SplitViewPresentable.swift +++ b/Riot/Modules/SplitView/SplitViewPresentable.swift @@ -18,6 +18,9 @@ import UIKit protocol SplitViewMasterPresentableDelegate: AnyObject { + /// Navigation router for the split view detail + var detailNavigationRouter: NavigationRouterType? { get } + /// Detail items from the split view var detailModules: [Presentable] { get } diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 8a57aebca..3739df274 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -28,6 +28,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private let parameters: TabBarCoordinatorParameters private let activityIndicatorPresenter: ActivityIndicatorPresenterType + private let indicatorPresenter: UserIndicatorTypePresenterProtocol // Indicate if the Coordinator has started once private var hasStartedOnce: Bool { @@ -74,6 +75,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.navigationRouter = NavigationRouter(navigationController: masterNavigationController) self.masterNavigationController = masterNavigationController self.activityIndicatorPresenter = ActivityIndicatorPresenter() + self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: masterNavigationController) } // MARK: - Public methods @@ -231,7 +233,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { homeViewController.accessibilityLabel = VectorL10n.titleHome if BuildSettings.useAppUserIndicators { - homeViewController.userIndicatorPresenter = AppUserIndicatorPresenter(appNavigator: parameters.appNavigator) + homeViewController.indicatorPresenter = UserIndicatorPresenterWrapper(presenter: indicatorPresenter) } let wrapperViewController = HomeViewControllerWithBannerWrapperViewController(viewController: homeViewController) @@ -412,7 +414,14 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } else { displayConfig = .default } + + var indicatorPresenter: UserIndicatorTypePresenterProtocol? + if BuildSettings.useAppUserIndicators, let detailNavigation = splitViewMasterPresentableDelegate?.detailNavigationRouter?.toPresentable() { + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: detailNavigation) + } + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + userIndicatorPresenter: indicatorPresenter, session: roomNavigationParameters.mxSession, roomId: roomNavigationParameters.roomId, eventId: roomNavigationParameters.eventId, @@ -682,8 +691,8 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { // For the moment when a room is left, reset the split detail with placeholder self.resetSplitViewDetails() if BuildSettings.useAppUserIndicators { - parameters.appNavigator - .addUserIndicator(.success(VectorL10n.roomParticipantsLeaveSuccess)) + indicatorPresenter + .present(.success(label: VectorL10n.roomParticipantsLeaveSuccess)) .store(in: &indicators) } } diff --git a/changelog.d/5603.change b/changelog.d/5603.change new file mode 100644 index 000000000..b688d36ad --- /dev/null +++ b/changelog.d/5603.change @@ -0,0 +1 @@ +Activity Indicators: Add updated indicators to room loading