From bf39fbf8deb9aaa590bb62a2843472ab4d2d0599 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 17 Feb 2022 11:10:55 +0000 Subject: [PATCH 1/6] Update activity indicators on leaving room Signed-off-by: Andy Uhnak --- CommonKit/Source/Activity/Activity.swift | 16 ++- Riot/Assets/en.lproj/Vector.strings | 3 + Riot/Categories/UIView+Toast.swift | 2 +- Riot/Generated/Strings.swift | 12 +++ Riot/Modules/Application/AppCoordinator.swift | 42 ++++++-- Riot/Modules/Application/AppNavigator.swift | 13 ++- .../AppActivityIndicatorPresenter.swift | 23 +++- ...FullscreenActivityIndicatorPresenter.swift | 53 ++++++++++ .../UIKit/LabelledActivityIndicatorView.swift | 100 ++++++++++++++++++ .../FullscreenLoadingActivityPresenter.swift | 74 +++++++++++++ .../ToastActivityPresenter.swift} | 34 +++--- .../Common/Recents/RecentsViewController.m | 12 ++- ...astView.swift => RectangleToastView.swift} | 2 +- ...ToastView.swift => RoundedToastView.swift} | 62 ++++++++--- .../RoomInfoListViewController.swift | 14 ++- Riot/Modules/Room/RoomViewController.m | 46 ++++---- Riot/Modules/TabBar/TabBarCoordinator.swift | 12 +++ changelog.d/5605.change | 1 + 18 files changed, 437 insertions(+), 84 deletions(-) create mode 100644 Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift create mode 100644 Riot/Modules/Common/ActivityIndicator/UIKit/LabelledActivityIndicatorView.swift create mode 100644 Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift rename Riot/Modules/Common/{Toasts/ActivityIndicatorToastPresenter.swift => ActivityPresenters/ToastActivityPresenter.swift} (63%) rename Riot/Modules/Common/Toasts/{BasicToastView.swift => RectangleToastView.swift} (98%) rename Riot/Modules/Common/Toasts/{ActivityIndicatorToastView.swift => RoundedToastView.swift} (59%) create mode 100644 changelog.d/5605.change diff --git a/CommonKit/Source/Activity/Activity.swift b/CommonKit/Source/Activity/Activity.swift index d72e878004..5c8eaac0b8 100644 --- a/CommonKit/Source/Activity/Activity.swift +++ b/CommonKit/Source/Activity/Activity.swift @@ -26,7 +26,7 @@ import UIKit /// A client that requests an activity can specify a default timeout after which the activity is dismissed, or it has to be manually /// responsible for dismissing it via `cancel` method, or by deallocating itself. public class Activity { - enum State { + public enum State { case pending case executing case completed @@ -35,7 +35,7 @@ public class Activity { private let request: ActivityRequest private let completion: () -> Void - private(set) var state: State + public private(set) var state: State public init(request: ActivityRequest, completion: @escaping () -> Void) { self.request = request @@ -45,7 +45,7 @@ public class Activity { } deinit { - cancel() + complete() } internal func start() { @@ -70,7 +70,7 @@ public class Activity { /// /// Note: clients can call this method directly, if they have access to the `Activity`. /// Once cancelled, `ActivityCenter` will automatically start the next `Activity` in the queue. - func cancel() { + public func cancel() { complete() } @@ -92,3 +92,11 @@ public extension Activity { collection.append(self) } } + +public extension Collection where Element == Activity { + func cancelAll() { + forEach { + $0.cancel() + } + } +} diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index ca75a5a65f..bf3887a305 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -290,6 +290,8 @@ Tap the + to start adding people."; "room_participants_leave_prompt_title_for_dm" = "Leave"; "room_participants_leave_prompt_msg" = "Are you sure you want to leave the room?"; "room_participants_leave_prompt_msg_for_dm" = "Are you sure you want to leave?"; +"room_participants_leave_processing" = "Leaving"; +"room_participants_leave_success" = "Left room"; "room_participants_remove_prompt_title" = "Confirmation"; "room_participants_remove_prompt_msg" = "Are you sure you want to remove %@ from this chat?"; "room_participants_remove_third_party_invite_prompt_msg" = "Are you sure you want to revoke this invite?"; @@ -1747,6 +1749,7 @@ Tap the + to start adding people."; "home_context_menu_low_priority" = "Low priority"; "home_context_menu_normal_priority" = "Normal priority"; "home_context_menu_leave" = "Leave"; +"home_syncing" = "Syncing"; // MARK: - Favourites diff --git a/Riot/Categories/UIView+Toast.swift b/Riot/Categories/UIView+Toast.swift index b89d63ac10..1d3205704f 100644 --- a/Riot/Categories/UIView+Toast.swift +++ b/Riot/Categories/UIView+Toast.swift @@ -60,7 +60,7 @@ extension UIView { duration: TimeInterval = Constants.defaultDuration, position: ToastPosition = Constants.defaultPosition, additionalMargin: CGFloat = 0.0) { - let view = BasicToastView(withMessage: message, image: image) + let view = RectangleToastView(withMessage: message, image: image) vc_toast(view: view, duration: duration, position: position, additionalMargin: additionalMargin) } diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1313a60288..37fa5173de 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1647,6 +1647,10 @@ public class VectorL10n: NSObject { public static func homeEmptyViewTitle(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "home_empty_view_title", p1, p2) } + /// Syncing + public static var homeSyncing: String { + return VectorL10n.tr("Vector", "home_syncing") + } /// Could not connect to the homeserver. public static var homeserverConnectionLost: String { return VectorL10n.tr("Vector", "homeserver_connection_lost") @@ -3687,6 +3691,10 @@ public class VectorL10n: NSObject { public static var roomParticipantsInvitedSection: String { return VectorL10n.tr("Vector", "room_participants_invited_section") } + /// Leaving + public static var roomParticipantsLeaveProcessing: String { + return VectorL10n.tr("Vector", "room_participants_leave_processing") + } /// Are you sure you want to leave the room? public static var roomParticipantsLeavePromptMsg: String { return VectorL10n.tr("Vector", "room_participants_leave_prompt_msg") @@ -3703,6 +3711,10 @@ public class VectorL10n: NSObject { public static var roomParticipantsLeavePromptTitleForDm: String { return VectorL10n.tr("Vector", "room_participants_leave_prompt_title_for_dm") } + /// Left room + public static var roomParticipantsLeaveSuccess: String { + return VectorL10n.tr("Vector", "room_participants_leave_success") + } /// %d participants public static func roomParticipantsMultiParticipants(_ p1: Int) -> String { return VectorL10n.tr("Vector", "room_participants_multi_participants", p1) diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 8f1b61d580..a30a69e683 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -322,15 +322,39 @@ fileprivate class AppNavigator: AppNavigatorProtocol { self.appCoordinator.navigate(to: destination) } - func addLoadingActivity() -> Activity { - let presenter = ActivityIndicatorToastPresenter( - text: VectorL10n.roomParticipantsSecurityLoading, - navigationController: appNavigationVC - ) - let request = ActivityRequest( - presenter: presenter, - dismissal: .manual - ) + func addAppActivity(_ type: AppActivityType) -> Activity { + let request = activityRequest(for: type) return ActivityCenter.shared.add(request) } + + // MARK: - Private + + private func activityRequest(for type: AppActivityType) -> ActivityRequest { + switch type { + case let .loading(label): + let presenter = ToastActivityPresenter( + viewState: .init( + style: .loading, + label: label + ), + navigationController: appNavigationVC + ) + return ActivityRequest( + presenter: presenter, + dismissal: .manual + ) + case let .success(label): + let presenter = ToastActivityPresenter( + viewState: .init( + style: .success, + label: label + ), + navigationController: appNavigationVC + ) + return ActivityRequest( + presenter: presenter, + dismissal: .timeout(1.5) + ) + } + } } diff --git a/Riot/Modules/Application/AppNavigator.swift b/Riot/Modules/Application/AppNavigator.swift index c33b572f49..030c25c048 100644 --- a/Riot/Modules/Application/AppNavigator.swift +++ b/Riot/Modules/Application/AppNavigator.swift @@ -17,6 +17,15 @@ import Foundation import CommonKit +/// Type of activity to be shown in the app navigator +enum AppActivityType { + /// 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 @@ -28,11 +37,11 @@ protocol AppNavigatorProtocol { /// 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 loading activity to an app-wide queue of other activitie + /// Add new activity, such as loading indicator or a success message, to an app-wide queue of other activities /// /// If the queue is empty, the activity will be displayed immediately, otherwise it will be pending /// until the previously added activities have completed / been cancelled. /// /// To remove an activity indicator, cancel or deallocate the returned `Activity` - func addLoadingActivity() -> Activity + func addAppActivity(_ type: AppActivityType) -> Activity } diff --git a/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift index 53a5d24538..d3bd3de280 100644 --- a/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift @@ -27,21 +27,36 @@ import CommonKit /// written in objective-c. @objc final class AppActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { private let appNavigator: AppNavigatorProtocol - private var activity: Activity? + private var loadingActivity: Activity? + private var otherActivities = [Activity]() init(appNavigator: AppNavigatorProtocol) { self.appNavigator = appNavigator } - + @objc func presentActivityIndicator() { - activity = appNavigator.addLoadingActivity() + presentActivityIndicator(label: VectorL10n.homeSyncing) + } + + @objc func presentActivityIndicator(label: String) { + guard loadingActivity == nil || loadingActivity?.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 activity indiciators if there is one already showing. + return + } + + loadingActivity = appNavigator.addAppActivity(.loading(label)) } @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { - activity = nil + loadingActivity = nil } func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { MXLog.error("[AppActivityIndicatorPresenter] Shared activity indicator does not support presenting from custom views") } + + @objc func presentSuccess(label: String) { + appNavigator.addAppActivity(.success(label)).store(in: &otherActivities) + } } diff --git a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift new file mode 100644 index 0000000000..7656e15fb5 --- /dev/null +++ b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift @@ -0,0 +1,53 @@ +// +// 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 activity / loading indicators, and conforming to legacy `ActivityIndicatorPresenterType`, +/// but interally wrapping an `ActivityPresenter` which is used in conjuction to `Activity` and `ActivityCenter`. +/// +/// Note: clients can skip using `FullscreenActivityIndicatorPresenter` and instead coordiinate 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 activity: Activity? + + init(label: String, on viewController: UIViewController) { + self.label = label + self.viewController = viewController + } + + func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { + guard let vc = viewController else { + return + } + + let request = ActivityRequest( + presenter: FullscreenLoadingActivityPresenter(label: label, on: vc), + dismissal: .manual + ) + + activity = ActivityCenter.shared.add(request) + } + + @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { + activity?.cancel() + } +} diff --git a/Riot/Modules/Common/ActivityIndicator/UIKit/LabelledActivityIndicatorView.swift b/Riot/Modules/Common/ActivityIndicator/UIKit/LabelledActivityIndicatorView.swift new file mode 100644 index 0000000000..266237bd48 --- /dev/null +++ b/Riot/Modules/Common/ActivityIndicator/UIKit/LabelledActivityIndicatorView.swift @@ -0,0 +1,100 @@ +// +// 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 + +final class LabelledActivityIndicatorView: UIView, Themable { + private enum Constants { + static let padding = UIEdgeInsets(top: 20, left: 40, bottom: 15, right: 40) + static let activityIndicatorScale = CGFloat(1.5) + static let cornerRadius: CGFloat = 12.0 + static let stackBackgroundOpacity: CGFloat = 0.9 + static let stackSpacing: CGFloat = 15 + static let backgroundOpacity: CGFloat = 0.5 + } + + private let stackBackgroundView: UIView = { + let view = UIView() + view.layer.cornerRadius = Constants.cornerRadius + view.alpha = Constants.stackBackgroundOpacity + return view + }() + + private let stackView: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.distribution = .fill + stack.alignment = .center + stack.spacing = Constants.stackSpacing + return stack + }() + + private let activityIndicator: UIActivityIndicatorView = { + let view = UIActivityIndicatorView() + view.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale) + view.startAnimating() + return view + }() + + private let label: UILabel = { + return UILabel() + }() + + init(text: String) { + super.init(frame: .zero) + setup(text: text) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup(text: String) { + setupStackView() + label.text = text + } + + private func setupStackView() { + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + + stackView.addArrangedSubview(activityIndicator) + stackView.addArrangedSubview(label) + + insertSubview(stackBackgroundView, belowSubview: stackView) + stackBackgroundView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackBackgroundView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Constants.padding.top), + stackBackgroundView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.padding.bottom), + stackBackgroundView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Constants.padding.left), + stackBackgroundView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Constants.padding.right) + ]) + } + + func update(theme: Theme) { + backgroundColor = theme.colors.primaryContent.withAlphaComponent(Constants.backgroundOpacity) + stackBackgroundView.backgroundColor = theme.colors.system + activityIndicator.color = theme.colors.secondaryContent + label.font = theme.fonts.calloutSB + label.textColor = theme.colors.secondaryContent + } +} diff --git a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift new file mode 100644 index 0000000000..0ca4cb4732 --- /dev/null +++ b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift @@ -0,0 +1,74 @@ +// +// 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 + +/// An `ActivityPresenter` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. +/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. +class FullscreenLoadingActivityPresenter: ActivityPresentable { + private let label: String + private weak var viewController: UIViewController? + private weak var view: UIView? + + init(label: String, on viewController: UIViewController) { + self.label = label + self.viewController = viewController + } + + func present() { + // Find the current top navigation controller + var vc: UIViewController? = viewController + while vc?.navigationController != nil { + vc = vc?.navigationController + } + guard let presentingVC = vc else { + return + } + + let view = LabelledActivityIndicatorView(text: label) + view.update(theme: ThemeService.shared().theme) + self.view = view + + view.translatesAutoresizingMaskIntoConstraints = false + presentingVC.view.addSubview(view) + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: presentingVC.view.topAnchor), + view.bottomAnchor.constraint(equalTo: presentingVC.view.bottomAnchor), + view.leadingAnchor.constraint(equalTo: presentingVC.view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: presentingVC.view.trailingAnchor) + ]) + + view.alpha = 0 + CATransaction.commit() + UIView.animate(withDuration: 0.2) { + view.alpha = 1 + } + } + + func dismiss() { + guard let view = view, view.superview != nil else { + return + } + + UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { + view.alpha = 0 + } completion: { _ in + view.removeFromSuperview() + } + } +} diff --git a/Riot/Modules/Common/Toasts/ActivityIndicatorToastPresenter.swift b/Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift similarity index 63% rename from Riot/Modules/Common/Toasts/ActivityIndicatorToastPresenter.swift rename to Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift index 1c592a2a24..f0bff4c175 100644 --- a/Riot/Modules/Common/Toasts/ActivityIndicatorToastPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift @@ -17,16 +17,17 @@ import Foundation import UIKit import CommonKit +import MatrixSDK -/// An `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators, and managed by an `Activity`, -/// meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. -class ActivityIndicatorToastPresenter: ActivityPresentable { - private let text: String +/// An `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators or success messages. +/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. +class ToastActivityPresenter: ActivityPresentable { + private let viewState: RoundedToastView.ViewState private weak var navigationController: UINavigationController? private weak var view: UIView? - init(text: String, navigationController: UINavigationController) { - self.text = text + init(viewState: RoundedToastView.ViewState, navigationController: UINavigationController) { + self.viewState = viewState self.navigationController = navigationController } @@ -35,7 +36,7 @@ class ActivityIndicatorToastPresenter: ActivityPresentable { return } - let view = ActivityIndicatorToastView(text: text) + let view = RoundedToastView(viewState: viewState) view.update(theme: ThemeService.shared().theme) self.view = view @@ -47,7 +48,9 @@ class ActivityIndicatorToastPresenter: ActivityPresentable { ]) view.alpha = 0 + CATransaction.flush() view.transform = .init(translationX: 0, y: 5) + UIView.animate(withDuration: 0.2) { view.alpha = 1 view.transform = .identity @@ -59,18 +62,11 @@ class ActivityIndicatorToastPresenter: ActivityPresentable { 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() - self.view = nil - } + 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.m b/Riot/Modules/Common/Recents/RecentsViewController.m index dbd2803379..ad09e064dc 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1280,8 +1280,7 @@ - (void)leaveEditedRoom MXRoom *room = [self.mainSession roomWithRoomId:currentRoomId]; if (room) { - [self startActivityIndicator]; - + [self startActivityIndicatorWithLabel:[VectorL10n roomParticipantsLeaveProcessing]]; // cancel pending uploads/downloads // they are useless by now [MXMediaManager cancelDownloadsInCacheFolder:room.roomId]; @@ -1296,6 +1295,7 @@ - (void)leaveEditedRoom { typeof(self) self = weakSelf; [self stopActivityIndicator]; + [self.activityPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]]; // Force table refresh [self cancelEditionMode:YES]; } @@ -2413,6 +2413,14 @@ - (BOOL)providesCustomActivityIndicator { return self.activityPresenter != nil; } +- (void)startActivityIndicatorWithLabel:(NSString *)label { + if (self.activityPresenter) { + [self.activityPresenter presentActivityIndicatorWithLabel:label]; + } else { + [super startActivityIndicator]; + } +} + - (void)startActivityIndicator { if (self.activityPresenter) { [self.activityPresenter presentActivityIndicator]; diff --git a/Riot/Modules/Common/Toasts/BasicToastView.swift b/Riot/Modules/Common/Toasts/RectangleToastView.swift similarity index 98% rename from Riot/Modules/Common/Toasts/BasicToastView.swift rename to Riot/Modules/Common/Toasts/RectangleToastView.swift index a4b9a39b69..41aa3c65fe 100644 --- a/Riot/Modules/Common/Toasts/BasicToastView.swift +++ b/Riot/Modules/Common/Toasts/RectangleToastView.swift @@ -17,7 +17,7 @@ import Foundation import UIKit -class BasicToastView: UIView, Themable { +class RectangleToastView: UIView, Themable { private enum Constants { static let padding: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) diff --git a/Riot/Modules/Common/Toasts/ActivityIndicatorToastView.swift b/Riot/Modules/Common/Toasts/RoundedToastView.swift similarity index 59% rename from Riot/Modules/Common/Toasts/ActivityIndicatorToastView.swift rename to Riot/Modules/Common/Toasts/RoundedToastView.swift index 6964ec445b..303bcbcec5 100644 --- a/Riot/Modules/Common/Toasts/ActivityIndicatorToastView.swift +++ b/Riot/Modules/Common/Toasts/RoundedToastView.swift @@ -18,14 +18,34 @@ import Foundation import UIKit import DesignKit -class ActivityIndicatorToastView: UIView, Themable { +class RoundedToastView: UIView, Themable { private struct Constants { static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) + static let activityIndicatorScale = CGFloat(0.75) + static let imageViewSize = CGFloat(15) static let shadowOffset = CGSize(width: 0, height: 4) static let shadowRadius = CGFloat(12) static let shadowOpacity = Float(0.1) } + private lazy var activityIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView() + indicator.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale) + indicator.startAnimating() + return indicator + }() + + private lazy var imagView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewSize), + imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize), + ]) + return imageView + }() + private let stackView: UIStackView = { let stack = UIStackView() stack.axis = .horizontal @@ -33,32 +53,25 @@ class ActivityIndicatorToastView: UIView, Themable { return stack }() - private let activityIndicator: UIActivityIndicatorView = { - let view = UIActivityIndicatorView() - view.transform = .init(scaleX: 0.75, y: 0.75) - view.startAnimating() - return view - }() - private let label: UILabel = { return UILabel() }() - init(text: String) { + init(viewState: ViewState) { super.init(frame: .zero) - setup(text: text) + setup(viewState: viewState) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setup(text: String) { + private func setup(viewState: ViewState) { setupLayer() setupStackView() - stackView.addArrangedSubview(activityIndicator) + stackView.addArrangedSubview(toastView(for: viewState.style)) stackView.addArrangedSubview(label) - label.text = text + label.text = viewState.label } private func setupStackView() { @@ -86,6 +99,29 @@ class ActivityIndicatorToastView: UIView, Themable { func update(theme: Theme) { backgroundColor = UIColor.white + stackView.arrangedSubviews.first?.tintColor = theme.colors.primaryContent label.font = theme.fonts.subheadline + label.textColor = theme.colors.primaryContent + } + + private func toastView(for style: Style) -> UIView { + switch style { + case .loading: + return activityIndicator + case .success: + imagView.image = Asset.Images.checkmark.image + return imagView + } + } +} + +extension RoundedToastView { + enum Style { + case loading + case success + } + struct ViewState { + let style: Style + let label: String } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 78b5af425b..030b7606ed 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -17,6 +17,8 @@ */ import UIKit +import CommonKit +import MatrixSDK final class RoomInfoListViewController: UIViewController { @@ -38,7 +40,7 @@ final class RoomInfoListViewController: UIViewController { private var viewModel: RoomInfoListViewModelType! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! - private var activityPresenter: ActivityIndicatorPresenter! + private var activityPresenter: ActivityIndicatorPresenterType! private var isRoomDirect: Bool = false private var screenTimer = AnalyticsScreenTimer(screen: .roomDetails) @@ -114,7 +116,14 @@ final class RoomInfoListViewController: UIViewController { // Do any additional setup after loading the view. self.setupViews() - self.activityPresenter = ActivityIndicatorPresenter() + if BuildSettings.appActivityIndicators { + self.activityPresenter = FullscreenActivityIndicatorPresenter( + label: VectorL10n.roomParticipantsLeaveProcessing, + on: self + ) + } else { + self.activityPresenter = ActivityIndicatorPresenter() + } self.errorPresenter = MXKErrorAlertPresentation() self.registerThemeServiceDidChangeThemeNotification() @@ -143,6 +152,7 @@ final class RoomInfoListViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) screenTimer.stop() + activityPresenter.removeCurrentActivityIndicator(animated: animated) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 4eddef5bcd..f7b6649ca8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1066,15 +1066,7 @@ - (void)leaveRoomOnEvent:(MXEvent*)event self.jumpToLastUnreadBannerContainer.hidden = YES; [super leaveRoomOnEvent:event]; - - if (self.delegate) - { - [self.delegate roomViewControllerDidLeaveRoom:self]; - } - else - { - [[AppDelegate theDelegate] restoreInitialDisplay:nil]; - } + [self notifyDelegateOnLeaveRoomIfNecessary]; } // Set the input toolbar according to the current display @@ -2192,16 +2184,7 @@ - (void)leaveRoom [self.roomDataSource.room leave:^{ [self stopActivityIndicator]; - - // We remove the current view controller. - if (self.delegate) - { - [self.delegate roomViewControllerDidLeaveRoom:self]; - } - else - { - [[AppDelegate theDelegate] restoreInitialDisplay:^{}]; - } + [self notifyDelegateOnLeaveRoomIfNecessary]; } failure:^(NSError *error) { @@ -2211,6 +2194,22 @@ - (void)leaveRoom }]; } +- (void)notifyDelegateOnLeaveRoomIfNecessary { + if (self.delegate) + { + // Leaving room often triggers multiple events, incl local delegate callbacks as well as global notifications, + // which may lead to multiple identical UI changes (navigating to home, displaying notification etc). + // To avoid this, as soon as we notify the delegate the first time, we nilify it, preventing future messages + // from being passed along, assuming that after leaving a room there is nothing else to communicate to the delegate. + [self.delegate roomViewControllerDidLeaveRoom:self]; + self.delegate = nil; + } + else + { + [[AppDelegate theDelegate] restoreInitialDisplay:^{}]; + } +} + - (void)roomPreviewDidTapCancelAction { // Decline this invitation = leave this page @@ -7052,14 +7051,7 @@ - (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter * - (void)roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter { - if (self.delegate) - { - [self.delegate roomViewControllerDidLeaveRoom:self]; - } - else - { - [[AppDelegate theDelegate] restoreInitialDisplay:nil]; - } + [self notifyDelegateOnLeaveRoomIfNecessary]; } #pragma mark - RemoveJitsiWidgetViewDelegate diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 30ce90bc08..4a7fc7c24a 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -17,6 +17,7 @@ */ import UIKit +import CommonKit @objcMembers final class TabBarCoordinator: NSObject, TabBarCoordinatorType { @@ -53,6 +54,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { return self.navigationRouter.modules.last is MasterTabBarController } + private var activities = [Activity]() + // MARK: Public // Must be used only internally @@ -73,6 +76,10 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.activityIndicatorPresenter = ActivityIndicatorPresenter() } + deinit { + activities.cancelAll() + } + // MARK: - Public methods func start() { @@ -702,6 +709,11 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { // For the moment when a room is left, reset the split detail with placeholder self.resetSplitViewDetails() + if BuildSettings.appActivityIndicators { + parameters.appNavigator + .addAppActivity(.success(VectorL10n.roomParticipantsLeaveSuccess)) + .store(in: &activities) + } } func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) { diff --git a/changelog.d/5605.change b/changelog.d/5605.change new file mode 100644 index 0000000000..05f78b91d3 --- /dev/null +++ b/changelog.d/5605.change @@ -0,0 +1 @@ +Activity Indicators: Update loading and success messages when leaving room From 9e0dbdde476b88d820328fa076482c44de27386f Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 21 Feb 2022 08:51:03 +0000 Subject: [PATCH 2/6] Use themed background color --- Riot/Modules/Common/Toasts/RoundedToastView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Common/Toasts/RoundedToastView.swift b/Riot/Modules/Common/Toasts/RoundedToastView.swift index 303bcbcec5..49aacb6bf1 100644 --- a/Riot/Modules/Common/Toasts/RoundedToastView.swift +++ b/Riot/Modules/Common/Toasts/RoundedToastView.swift @@ -98,7 +98,7 @@ class RoundedToastView: UIView, Themable { } func update(theme: Theme) { - backgroundColor = UIColor.white + backgroundColor = theme.colors.background stackView.arrangedSubviews.first?.tintColor = theme.colors.primaryContent label.font = theme.fonts.subheadline label.textColor = theme.colors.primaryContent From 45a4e12311d7b96976311fcff0d05cd8579ba1fa Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 22 Feb 2022 15:26:51 +0000 Subject: [PATCH 3/6] Rename Activity to UserIndicator --- .../Tests/UserIndicatorPresenterSpy.swift} | 2 +- .../Tests/UserIndicatorQueueTests.swift} | 32 ++++---- .../Tests/UserIndicatorTests.swift} | 74 +++++++++---------- .../UserIndicator.swift} | 26 +++---- .../UserIndicatorDismissal.swift} | 8 +- .../UserIndicatorPresentable.swift} | 8 +- .../UserIndicatorQueue.swift} | 30 ++++---- .../UserIndicatorRequest.swift} | 10 +-- Riot/Modules/Application/AppCoordinator.swift | 17 +++-- Riot/Modules/Application/AppNavigator.swift | 14 ++-- ....swift => AppUserIndicatorPresenter.swift} | 24 +++--- ...FullscreenActivityIndicatorPresenter.swift | 14 ++-- ...FullscreenLoadingIndicatorPresenter.swift} | 6 +- ...wift => ToastUserIndicatorPresenter.swift} | 6 +- .../Common/Recents/RecentsViewController.h | 6 +- .../Common/Recents/RecentsViewController.m | 16 ++-- Riot/Modules/TabBar/TabBarCoordinator.swift | 10 +-- 17 files changed, 152 insertions(+), 151 deletions(-) rename CommonKit/Source/{Activity/Tests/ActivityPresenterSpy.swift => UserIndicators/Tests/UserIndicatorPresenterSpy.swift} (92%) rename CommonKit/Source/{Activity/Tests/ActivityCenterTests.swift => UserIndicators/Tests/UserIndicatorQueueTests.swift} (56%) rename CommonKit/Source/{Activity/Tests/ActivityTests.swift => UserIndicators/Tests/UserIndicatorTests.swift} (56%) rename CommonKit/Source/{Activity/Activity.swift => UserIndicators/UserIndicator.swift} (65%) rename CommonKit/Source/{Activity/ActivityDismissal.swift => UserIndicators/UserIndicatorDismissal.swift} (69%) rename CommonKit/Source/{Activity/ActivityPresentable.swift => UserIndicators/UserIndicatorPresentable.swift} (65%) rename CommonKit/Source/{Activity/ActivityCenter.swift => UserIndicators/UserIndicatorQueue.swift} (52%) rename CommonKit/Source/{Activity/ActivityRequest.swift => UserIndicators/UserIndicatorRequest.swift} (65%) rename Riot/Modules/Common/ActivityIndicator/{AppActivityIndicatorPresenter.swift => AppUserIndicatorPresenter.swift} (60%) rename Riot/Modules/Common/ActivityPresenters/{FullscreenLoadingActivityPresenter.swift => FullscreenLoadingIndicatorPresenter.swift} (86%) rename Riot/Modules/Common/ActivityPresenters/{ToastActivityPresenter.swift => ToastUserIndicatorPresenter.swift} (87%) diff --git a/CommonKit/Source/Activity/Tests/ActivityPresenterSpy.swift b/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift similarity index 92% rename from CommonKit/Source/Activity/Tests/ActivityPresenterSpy.swift rename to CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift index 2a0d032494..8be85ff9c9 100644 --- a/CommonKit/Source/Activity/Tests/ActivityPresenterSpy.swift +++ b/CommonKit/Source/UserIndicators/Tests/UserIndicatorPresenterSpy.swift @@ -16,7 +16,7 @@ import Foundation -class ActivityPresenterSpy: ActivityPresentable { +class UserIndicatorPresenterSpy: UserIndicatorPresentable { var intel = [String]() func present() { diff --git a/CommonKit/Source/Activity/Tests/ActivityCenterTests.swift b/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift similarity index 56% rename from CommonKit/Source/Activity/Tests/ActivityCenterTests.swift rename to CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift index a1e375851c..0b937b8a82 100644 --- a/CommonKit/Source/Activity/Tests/ActivityCenterTests.swift +++ b/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift @@ -17,34 +17,34 @@ import Foundation import XCTest -class ActivityCenterTests: XCTestCase { - var activities: [Activity]! - var center: ActivityCenter! +class UserIndicatorQueueTests: XCTestCase { + var indicators: [UserIndicator]! + var center: UserIndicatorQueue! override func setUp() { - activities = [] - center = ActivityCenter() + indicators = [] + center = UserIndicatorQueue() } - func makeRequest() -> ActivityRequest { - return ActivityRequest( - presenter: ActivityPresenterSpy(), + func makeRequest() -> UserIndicatorRequest { + return UserIndicatorRequest( + presenter: UserIndicatorPresenterSpy(), dismissal: .manual ) } - func testStartsActivityWhenAdded() { - let activity = center.add(makeRequest()) - XCTAssertEqual(activity.state, .executing) + func testStartsIndicatorWhenAdded() { + let indicator = center.add(makeRequest()) + XCTAssertEqual(indicator.state, .executing) } - func testSecondActivityIsPending() { - center.add(makeRequest()).store(in: &activities) - let activity = center.add(makeRequest()) - XCTAssertEqual(activity.state, .pending) + func testSecondIndicatorIsPending() { + center.add(makeRequest()).store(in: &indicators) + let indicator = center.add(makeRequest()) + XCTAssertEqual(indicator.state, .pending) } - func testSecondActivityIsExecutingWhenFirstCompleted() { + func testSecondIndicatorIsExecutingWhenFirstCompleted() { let first = center.add(makeRequest()) let second = center.add(makeRequest()) diff --git a/CommonKit/Source/Activity/Tests/ActivityTests.swift b/CommonKit/Source/UserIndicators/Tests/UserIndicatorTests.swift similarity index 56% rename from CommonKit/Source/Activity/Tests/ActivityTests.swift rename to CommonKit/Source/UserIndicators/Tests/UserIndicatorTests.swift index 11cc7eec29..e0722af8fa 100644 --- a/CommonKit/Source/Activity/Tests/ActivityTests.swift +++ b/CommonKit/Source/UserIndicators/Tests/UserIndicatorTests.swift @@ -17,20 +17,20 @@ import Foundation import XCTest -class ActivityTests: XCTestCase { - var presenter: ActivityPresenterSpy! +class UserIndicatorTests: XCTestCase { + var presenter: UserIndicatorPresenterSpy! override func setUp() { super.setUp() - presenter = ActivityPresenterSpy() + presenter = UserIndicatorPresenterSpy() } - func makeActivity(dismissal: ActivityDismissal = .manual, callback: @escaping () -> Void = {}) -> Activity { - let request = ActivityRequest( + func makeIndicator(dismissal: UserIndicatorDismissal = .manual, callback: @escaping () -> Void = {}) -> UserIndicator { + let request = UserIndicatorRequest( presenter: presenter, dismissal: dismissal ) - return Activity( + return UserIndicator( request: request, completion: callback ) @@ -38,58 +38,58 @@ class ActivityTests: XCTestCase { // MARK: - State - func testNewActivityIsPending() { - let activity = makeActivity() - XCTAssertEqual(activity.state, .pending) + func testNewIndicatorIsPending() { + let indicator = makeIndicator() + XCTAssertEqual(indicator.state, .pending) } - func testStartedActivityIsExecuting() { - let activity = makeActivity() - activity.start() - XCTAssertEqual(activity.state, .executing) + func testStartedIndicatorIsExecuting() { + let indicator = makeIndicator() + indicator.start() + XCTAssertEqual(indicator.state, .executing) } - func testCancelledActivityIsCompleted() { - let activity = makeActivity() - activity.cancel() - XCTAssertEqual(activity.state, .completed) + func testCancelledIndicatorIsCompleted() { + let indicator = makeIndicator() + indicator.cancel() + XCTAssertEqual(indicator.state, .completed) } // MARK: - Presenter - func testStartingActivityPresentsUI() { - let activity = makeActivity() - activity.start() + func testStartingIndicatorPresentsUI() { + let indicator = makeIndicator() + indicator.start() XCTAssertEqual(presenter.intel, ["present()"]) } func testAllowStartingOnlyOnce() { - let activity = makeActivity() - activity.start() + let indicator = makeIndicator() + indicator.start() presenter.intel = [] - activity.start() + indicator.start() XCTAssertEqual(presenter.intel, []) } - func testCancellingActivityDismissesUI() { - let activity = makeActivity() - activity.start() + func testCancellingIndicatorDismissesUI() { + let indicator = makeIndicator() + indicator.start() presenter.intel = [] - activity.cancel() + indicator.cancel() XCTAssertEqual(presenter.intel, ["dismiss()"]) } func testAllowCancellingOnlyOnce() { - let activity = makeActivity() - activity.start() - activity.cancel() + let indicator = makeIndicator() + indicator.start() + indicator.cancel() presenter.intel = [] - activity.cancel() + indicator.cancel() XCTAssertEqual(presenter.intel, []) } @@ -98,9 +98,9 @@ class ActivityTests: XCTestCase { func testDismissAfterTimeout() { let interval: TimeInterval = 0.01 - let activity = makeActivity(dismissal: .timeout(interval)) + let indicator = makeIndicator(dismissal: .timeout(interval)) - activity.start() + indicator.start() let exp = expectation(description: "") DispatchQueue.main.asyncAfter(deadline: .now() + interval) { @@ -108,19 +108,19 @@ class ActivityTests: XCTestCase { } waitForExpectations(timeout: 1) - XCTAssertEqual(activity.state, .completed) + XCTAssertEqual(indicator.state, .completed) } // MARK: - Completion callback func testTriggersCallbackWhenCompleted() { var didComplete = false - let activity = makeActivity { + let indicator = makeIndicator { didComplete = true } - activity.start() + indicator.start() - activity.cancel() + indicator.cancel() XCTAssertTrue(didComplete) } diff --git a/CommonKit/Source/Activity/Activity.swift b/CommonKit/Source/UserIndicators/UserIndicator.swift similarity index 65% rename from CommonKit/Source/Activity/Activity.swift rename to CommonKit/Source/UserIndicators/UserIndicator.swift index 5c8eaac0b8..72338013d4 100644 --- a/CommonKit/Source/Activity/Activity.swift +++ b/CommonKit/Source/UserIndicators/UserIndicator.swift @@ -17,27 +17,27 @@ import Foundation import UIKit -/// An `Activity` represents the state of a temporary visual indicator, such as activity indicator, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter` +/// A `UserIndicator` represents the state of a temporary visual indicator, such as loading spinner, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter` /// whenever the UI should be shown or hidden. /// -/// More than one `Activity` may be requested by the system at the same time (e.g. global syncing vs local refresh), -/// and the `ActivityCenter` will ensure that only one activity is shown at a given time, putting the other in a pending queue. +/// More than one `UserIndicator` may be requested by the system at the same time (e.g. global syncing vs local refresh), +/// and the `UserIndicatorQueue` will ensure that only one indicator is shown at a given time, putting the other in a pending queue. /// -/// A client that requests an activity can specify a default timeout after which the activity is dismissed, or it has to be manually +/// A client that requests an indicator can specify a default timeout after which the indicator is dismissed, or it has to be manually /// responsible for dismissing it via `cancel` method, or by deallocating itself. -public class Activity { +public class UserIndicator { public enum State { case pending case executing case completed } - private let request: ActivityRequest + private let request: UserIndicatorRequest private let completion: () -> Void public private(set) var state: State - public init(request: ActivityRequest, completion: @escaping () -> Void) { + public init(request: UserIndicatorRequest, completion: @escaping () -> Void) { self.request = request self.completion = completion @@ -66,10 +66,10 @@ public class Activity { } } - /// Cancel the activity, triggering any dismissal action / animation + /// Cancel the indicator, triggering any dismissal action / animation /// - /// Note: clients can call this method directly, if they have access to the `Activity`. - /// Once cancelled, `ActivityCenter` will automatically start the next `Activity` in the queue. + /// Note: clients can call this method directly, if they have access to the `UserIndicator`. + /// Once cancelled, `UserIndicatorQueue` will automatically start the next `UserIndicator` in the queue. public func cancel() { complete() } @@ -87,13 +87,13 @@ public class Activity { } } -public extension Activity { - func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == Activity { +public extension UserIndicator { + func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator { collection.append(self) } } -public extension Collection where Element == Activity { +public extension Collection where Element == UserIndicator { func cancelAll() { forEach { $0.cancel() diff --git a/CommonKit/Source/Activity/ActivityDismissal.swift b/CommonKit/Source/UserIndicators/UserIndicatorDismissal.swift similarity index 69% rename from CommonKit/Source/Activity/ActivityDismissal.swift rename to CommonKit/Source/UserIndicators/UserIndicatorDismissal.swift index 69405c579c..874a4f1ede 100644 --- a/CommonKit/Source/Activity/ActivityDismissal.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorDismissal.swift @@ -16,10 +16,10 @@ import Foundation -/// Different ways in which an `Activity` can be dismissed -public enum ActivityDismissal { - /// The `Activity` will not manage the dismissal, but will expect the calling client to do so manually +/// Different ways in which a `UserIndicator` can be dismissed +public enum UserIndicatorDismissal { + /// The `UserIndicator` will not manage the dismissal, but will expect the calling client to do so manually case manual - /// The `Activity` will be automatically dismissed after `TimeInterval` + /// The `UserIndicator` will be automatically dismissed after `TimeInterval` case timeout(TimeInterval) } diff --git a/CommonKit/Source/Activity/ActivityPresentable.swift b/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift similarity index 65% rename from CommonKit/Source/Activity/ActivityPresentable.swift rename to CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift index 5bb489fcc0..00a90913e5 100644 --- a/CommonKit/Source/Activity/ActivityPresentable.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift @@ -16,10 +16,10 @@ import Foundation -/// A presenter associated with and called by an `Activity`, and responsible for the underlying view shown on the screen. -public protocol ActivityPresentable { - /// Called when the `Activity` is started (manually or by the `ActivityCenter`) +/// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen. +public protocol UserIndicatorPresentable { + /// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`) func present() - /// Called when the `Activity` is manually cancelled or completed + /// Called when the `UserIndicator` is manually cancelled or completed func dismiss() } diff --git a/CommonKit/Source/Activity/ActivityCenter.swift b/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift similarity index 52% rename from CommonKit/Source/Activity/ActivityCenter.swift rename to CommonKit/Source/UserIndicators/UserIndicatorQueue.swift index 9d9e0c7044..b66014c0f0 100644 --- a/CommonKit/Source/Activity/ActivityCenter.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorQueue.swift @@ -16,11 +16,11 @@ import Foundation -/// A shared activity center with a single FIFO queue which will ensure only one activity is shown at a given time. +/// A FIFO queue which will ensure only one user indicator is shown at a given time. /// -/// `ActivityCenter` offers a `shared` center that can be used by any clients, but clients are also allowed -/// to create local `ActivityCenter` if the context requres multiple simultaneous activities. -public class ActivityCenter { +/// `UserIndicatorQueue` offers a `shared` queue that can be used by any clients app-wide, but clients are also allowed +/// to create local `UserIndicatorQueue` if the context requres multiple simultaneous indicators. +public class UserIndicatorQueue { private class Weak { weak var element: T? init(_ element: T) { @@ -28,27 +28,27 @@ public class ActivityCenter { } } - public static let shared = ActivityCenter() - private var queue = [Weak]() + public static let shared = UserIndicatorQueue() + private var queue = [Weak]() - /// Add a new activity to the queue by providing a request. + /// Add a new indicator to the queue by providing a request. /// - /// The queue will start the activity right away, if there are no currently running activities, - /// otherwise the activity will be put on hold. - public func add(_ request: ActivityRequest) -> Activity { - let activity = Activity(request: request) { [weak self] in + /// The queue will start the indicator right away, if there are no currently running indicators, + /// otherwise the indicator will be put on hold. + public func add(_ request: UserIndicatorRequest) -> UserIndicator { + let indicator = UserIndicator(request: request) { [weak self] in self?.startNextIfIdle() } - queue.append(Weak(activity)) + queue.append(Weak(indicator)) startNextIfIdle() - return activity + return indicator } private func startNextIfIdle() { cleanup() - if let activity = queue.first?.element, activity.state == .pending { - activity.start() + if let indicator = queue.first?.element, indicator.state == .pending { + indicator.start() } } diff --git a/CommonKit/Source/Activity/ActivityRequest.swift b/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift similarity index 65% rename from CommonKit/Source/Activity/ActivityRequest.swift rename to CommonKit/Source/UserIndicators/UserIndicatorRequest.swift index f009fc211d..83e882c0ec 100644 --- a/CommonKit/Source/Activity/ActivityRequest.swift +++ b/CommonKit/Source/UserIndicators/UserIndicatorRequest.swift @@ -16,12 +16,12 @@ import Foundation -/// A request used to create an underlying `Activity`, allowing clients to only specify the visual aspects of an activity. -public struct ActivityRequest { - internal let presenter: ActivityPresentable - internal let dismissal: ActivityDismissal +/// 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 dismissal: UserIndicatorDismissal - public init(presenter: ActivityPresentable, dismissal: ActivityDismissal) { + public init(presenter: UserIndicatorPresentable, dismissal: UserIndicatorDismissal) { self.presenter = presenter self.dismissal = dismissal } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index a30a69e683..93ae25fe5a 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -322,39 +322,40 @@ fileprivate class AppNavigator: AppNavigatorProtocol { self.appCoordinator.navigate(to: destination) } - func addAppActivity(_ type: AppActivityType) -> Activity { - let request = activityRequest(for: type) - return ActivityCenter.shared.add(request) + func addUserIndicator(_ type: AppUserIndicatorType) -> UserIndicator { + let request = userIndicatorRequest(for: type) + return UserIndicatorQueue.shared.add(request) } // MARK: - Private - private func activityRequest(for type: AppActivityType) -> ActivityRequest { + private func userIndicatorRequest(for type: AppUserIndicatorType) -> UserIndicatorRequest { switch type { case let .loading(label): - let presenter = ToastActivityPresenter( + let presenter = ToastUserIndicatorPresenter( viewState: .init( style: .loading, label: label ), navigationController: appNavigationVC ) - return ActivityRequest( + return UserIndicatorRequest( presenter: presenter, dismissal: .manual ) case let .success(label): - let presenter = ToastActivityPresenter( + let presenter = ToastUserIndicatorPresenter( viewState: .init( style: .success, label: label ), navigationController: appNavigationVC ) - return ActivityRequest( + return UserIndicatorRequest( presenter: presenter, dismissal: .timeout(1.5) ) } } } + diff --git a/Riot/Modules/Application/AppNavigator.swift b/Riot/Modules/Application/AppNavigator.swift index 030c25c048..dd0eff1ebb 100644 --- a/Riot/Modules/Application/AppNavigator.swift +++ b/Riot/Modules/Application/AppNavigator.swift @@ -17,8 +17,8 @@ import Foundation import CommonKit -/// Type of activity to be shown in the app navigator -enum AppActivityType { +/// Type of indicator to be shown in the app navigator +enum AppUserIndicatorType { /// Loading toast with custom label case loading(String) @@ -37,11 +37,11 @@ protocol AppNavigatorProtocol { /// 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 activity, such as loading indicator or a success message, to an app-wide queue of other activities + /// 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 activity will be displayed immediately, otherwise it will be pending - /// until the previously added activities have completed / been cancelled. + /// 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 activity indicator, cancel or deallocate the returned `Activity` - func addAppActivity(_ type: AppActivityType) -> Activity + /// To remove an indicator, cancel or deallocate the returned `UserIndicator` + func addUserIndicator(_ type: AppUserIndicatorType) -> UserIndicator } diff --git a/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift similarity index 60% rename from Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift rename to Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift index d3bd3de280..245b5e3231 100644 --- a/Riot/Modules/Common/ActivityIndicator/AppActivityIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityIndicator/AppUserIndicatorPresenter.swift @@ -19,16 +19,16 @@ import UIKit import MatrixSDK import CommonKit -/// Presenter which displays activity / loading indicators using app-wide `AppNavigator`, thus displaying them in a unified way, -/// and `ActivityCenter`/`Activity`, which ensures that only one activity is shown at a given time. +/// 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 `AppActivityIndicatorPresenter` and instead coordiinate with `AppNavigatorProtocol` directly. +/// 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 AppActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { +@objc final class AppUserIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { private let appNavigator: AppNavigatorProtocol - private var loadingActivity: Activity? - private var otherActivities = [Activity]() + private var loadingIndicator: UserIndicator? + private var otherIndicators = [UserIndicator]() init(appNavigator: AppNavigatorProtocol) { self.appNavigator = appNavigator @@ -39,24 +39,24 @@ import CommonKit } @objc func presentActivityIndicator(label: String) { - guard loadingActivity == nil || loadingActivity?.state == .completed else { + 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 activity indiciators if there is one already showing. + // so there is no reason to keep adding new indiciators if there is one already showing. return } - loadingActivity = appNavigator.addAppActivity(.loading(label)) + loadingIndicator = appNavigator.addUserIndicator(.loading(label)) } @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { - loadingActivity = nil + loadingIndicator = nil } func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { - MXLog.error("[AppActivityIndicatorPresenter] Shared activity indicator does not support presenting from custom views") + MXLog.error("[AppUserIndicatorPresenter] Shared indicator presenter does not support presenting from custom views") } @objc func presentSuccess(label: String) { - appNavigator.addAppActivity(.success(label)).store(in: &otherActivities) + appNavigator.addUserIndicator(.success(label)).store(in: &otherIndicators) } } diff --git a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift index 7656e15fb5..bf8722202d 100644 --- a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift @@ -18,8 +18,8 @@ import Foundation import CommonKit import UIKit -/// Presenter which displays fullscreen activity / loading indicators, and conforming to legacy `ActivityIndicatorPresenterType`, -/// but interally wrapping an `ActivityPresenter` which is used in conjuction to `Activity` and `ActivityCenter`. +/// 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 coordiinate with `AppNavigatorProtocol` directly. /// The presenter exists mostly as a transition for view controllers already using `ActivityIndicatorPresenterType` and / or view controllers @@ -27,7 +27,7 @@ import UIKit @objc final class FullscreenActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType { private let label: String private weak var viewController: UIViewController? - private var activity: Activity? + private var indicator: UserIndicator? init(label: String, on viewController: UIViewController) { self.label = label @@ -39,15 +39,15 @@ import UIKit return } - let request = ActivityRequest( - presenter: FullscreenLoadingActivityPresenter(label: label, on: vc), + let request = UserIndicatorRequest( + presenter: FullscreenLoadingIndicatorPresenter(label: label, on: vc), dismissal: .manual ) - activity = ActivityCenter.shared.add(request) + indicator = UserIndicatorQueue.shared.add(request) } @objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) { - activity?.cancel() + indicator?.cancel() } } diff --git a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift similarity index 86% rename from Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift rename to Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift index 0ca4cb4732..8ca7fb026f 100644 --- a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingActivityPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift @@ -18,9 +18,9 @@ import Foundation import CommonKit import UIKit -/// An `ActivityPresenter` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. -/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. -class FullscreenLoadingActivityPresenter: ActivityPresentable { +/// A `UserIndicatorPresentable` 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 { private let label: String private weak var viewController: UIViewController? private weak var view: UIView? diff --git a/Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift similarity index 87% rename from Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift rename to Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift index f0bff4c175..f975cd651c 100644 --- a/Riot/Modules/Common/ActivityPresenters/ToastActivityPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift @@ -19,9 +19,9 @@ import UIKit import CommonKit import MatrixSDK -/// An `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators or success messages. -/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes. -class ToastActivityPresenter: ActivityPresentable { +/// 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: RoundedToastView.ViewState private weak var navigationController: UINavigationController? private weak var view: UIView? diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index 00f728d243..9c29a28f87 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -19,7 +19,7 @@ @class RootTabEmptyView; @class AnalyticsScreenTimer; -@class AppActivityIndicatorPresenter; +@class AppUserIndicatorPresenter; /** Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. @@ -98,9 +98,9 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; @property (nonatomic) AnalyticsScreenTimer *screenTimer; /** - Presenter for displaying app-wide activity / loading indicators. If not set, the view controller will use legacy activity indicators + Presenter for displaying app-wide user indicators. If not set, the view controller will use legacy activity indicators */ -@property (nonatomic, strong) AppActivityIndicatorPresenter *activityPresenter; +@property (nonatomic, strong) AppUserIndicatorPresenter *userIndicatorPresenter; /** 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 ad09e064dc..722b706a2a 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1295,7 +1295,7 @@ - (void)leaveEditedRoom { typeof(self) self = weakSelf; [self stopActivityIndicator]; - [self.activityPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]]; + [self.userIndicatorPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]]; // Force table refresh [self cancelEditionMode:YES]; } @@ -2410,28 +2410,28 @@ -(void)roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(Ro #pragma mark - Activity Indicator - (BOOL)providesCustomActivityIndicator { - return self.activityPresenter != nil; + return self.userIndicatorPresenter != nil; } - (void)startActivityIndicatorWithLabel:(NSString *)label { - if (self.activityPresenter) { - [self.activityPresenter presentActivityIndicatorWithLabel:label]; + if (self.userIndicatorPresenter) { + [self.userIndicatorPresenter presentActivityIndicatorWithLabel:label]; } else { [super startActivityIndicator]; } } - (void)startActivityIndicator { - if (self.activityPresenter) { - [self.activityPresenter presentActivityIndicator]; + if (self.userIndicatorPresenter) { + [self.userIndicatorPresenter presentActivityIndicator]; } else { [super startActivityIndicator]; } } - (void)stopActivityIndicator { - if (self.activityPresenter) { - [self.activityPresenter removeCurrentActivityIndicatorWithAnimated:YES completion:nil]; + if (self.userIndicatorPresenter) { + [self.userIndicatorPresenter removeCurrentActivityIndicatorWithAnimated:YES completion:nil]; } else { [super stopActivityIndicator]; } diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 4a7fc7c24a..4fd95c2512 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -54,7 +54,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { return self.navigationRouter.modules.last is MasterTabBarController } - private var activities = [Activity]() + private var indicators = [UserIndicator]() // MARK: Public @@ -77,7 +77,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } deinit { - activities.cancelAll() + indicators.cancelAll() } // MARK: - Public methods @@ -235,7 +235,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { homeViewController.accessibilityLabel = VectorL10n.titleHome if BuildSettings.appActivityIndicators { - homeViewController.activityPresenter = AppActivityIndicatorPresenter(appNavigator: parameters.appNavigator) + homeViewController.userIndicatorPresenter = AppUserIndicatorPresenter(appNavigator: parameters.appNavigator) } let wrapperViewController = HomeViewControllerWithBannerWrapperViewController(viewController: homeViewController) @@ -711,8 +711,8 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { self.resetSplitViewDetails() if BuildSettings.appActivityIndicators { parameters.appNavigator - .addAppActivity(.success(VectorL10n.roomParticipantsLeaveSuccess)) - .store(in: &activities) + .addUserIndicator(.success(VectorL10n.roomParticipantsLeaveSuccess)) + .store(in: &indicators) } } From 87302914104b4ff7dd2ca1e15bd2b84f02f3bdd3 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 22 Feb 2022 16:21:11 +0000 Subject: [PATCH 4/6] Extract toast view state --- ...FullscreenActivityIndicatorPresenter.swift | 2 +- .../ToastUserIndicatorPresenter.swift | 4 +-- .../Common/Toasts/RoundedToastView.swift | 25 +++++------------ .../Common/Toasts/ToastViewState.swift | 27 +++++++++++++++++++ 4 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 Riot/Modules/Common/Toasts/ToastViewState.swift diff --git a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift index bf8722202d..b5f7cfb144 100644 --- a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift @@ -21,7 +21,7 @@ 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 coordiinate with `AppNavigatorProtocol` directly. +/// 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 { diff --git a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift index f975cd651c..281bb92af1 100644 --- a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift @@ -22,11 +22,11 @@ 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: RoundedToastView.ViewState + private let viewState: ToastViewState private weak var navigationController: UINavigationController? private weak var view: UIView? - init(viewState: RoundedToastView.ViewState, navigationController: UINavigationController) { + init(viewState: ToastViewState, navigationController: UINavigationController) { self.viewState = viewState self.navigationController = navigationController } diff --git a/Riot/Modules/Common/Toasts/RoundedToastView.swift b/Riot/Modules/Common/Toasts/RoundedToastView.swift index 49aacb6bf1..927c608f76 100644 --- a/Riot/Modules/Common/Toasts/RoundedToastView.swift +++ b/Riot/Modules/Common/Toasts/RoundedToastView.swift @@ -35,13 +35,13 @@ class RoundedToastView: UIView, Themable { return indicator }() - private lazy var imagView: UIImageView = { + private lazy var imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewSize), - imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize), + imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize) ]) return imageView }() @@ -57,7 +57,7 @@ class RoundedToastView: UIView, Themable { return UILabel() }() - init(viewState: ViewState) { + init(viewState: ToastViewState) { super.init(frame: .zero) setup(viewState: viewState) } @@ -66,7 +66,7 @@ class RoundedToastView: UIView, Themable { fatalError("init(coder:) has not been implemented") } - private func setup(viewState: ViewState) { + private func setup(viewState: ToastViewState) { setupLayer() setupStackView() stackView.addArrangedSubview(toastView(for: viewState.style)) @@ -104,24 +104,13 @@ class RoundedToastView: UIView, Themable { label.textColor = theme.colors.primaryContent } - private func toastView(for style: Style) -> UIView { + private func toastView(for style: ToastViewState.Style) -> UIView { switch style { case .loading: return activityIndicator case .success: - imagView.image = Asset.Images.checkmark.image - return imagView + imageView.image = Asset.Images.checkmark.image + return imageView } } } - -extension RoundedToastView { - enum Style { - case loading - case success - } - struct ViewState { - let style: Style - let label: String - } -} diff --git a/Riot/Modules/Common/Toasts/ToastViewState.swift b/Riot/Modules/Common/Toasts/ToastViewState.swift new file mode 100644 index 0000000000..e241d46453 --- /dev/null +++ b/Riot/Modules/Common/Toasts/ToastViewState.swift @@ -0,0 +1,27 @@ +// +// 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 + +struct ToastViewState { + enum Style { + case loading + case success + } + + let style: Style + let label: String +} From ef65dc113ef121666c41666df43303a9f1850632 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 22 Feb 2022 16:44:22 +0000 Subject: [PATCH 5/6] Rename properties and arguments --- .../Tests/UserIndicatorQueueTests.swift | 14 ++++++------- Config/BuildSettings.swift | 2 +- ...FullscreenActivityIndicatorPresenter.swift | 6 +++--- .../FullscreenLoadingIndicatorPresenter.swift | 20 +++++++++---------- .../RoomInfoListViewController.swift | 4 ++-- Riot/Modules/TabBar/TabBarCoordinator.swift | 8 ++------ 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift b/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift index 0b937b8a82..50c2e458d4 100644 --- a/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift +++ b/CommonKit/Source/UserIndicators/Tests/UserIndicatorQueueTests.swift @@ -19,11 +19,11 @@ import XCTest class UserIndicatorQueueTests: XCTestCase { var indicators: [UserIndicator]! - var center: UserIndicatorQueue! + var queue: UserIndicatorQueue! override func setUp() { indicators = [] - center = UserIndicatorQueue() + queue = UserIndicatorQueue() } func makeRequest() -> UserIndicatorRequest { @@ -34,19 +34,19 @@ class UserIndicatorQueueTests: XCTestCase { } func testStartsIndicatorWhenAdded() { - let indicator = center.add(makeRequest()) + let indicator = queue.add(makeRequest()) XCTAssertEqual(indicator.state, .executing) } func testSecondIndicatorIsPending() { - center.add(makeRequest()).store(in: &indicators) - let indicator = center.add(makeRequest()) + queue.add(makeRequest()).store(in: &indicators) + let indicator = queue.add(makeRequest()) XCTAssertEqual(indicator.state, .pending) } func testSecondIndicatorIsExecutingWhenFirstCompleted() { - let first = center.add(makeRequest()) - let second = center.add(makeRequest()) + let first = queue.add(makeRequest()) + let second = queue.add(makeRequest()) first.cancel() diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 3eb9c33a6b..fe9679106f 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -211,7 +211,7 @@ final class BuildSettings: NSObject { static let allowInviteExernalUsers: Bool = true /// Whether a screen uses legacy local activity indicators or improved app-wide indicators - static var appActivityIndicators: Bool { + static var useAppUserIndicators: Bool { #if DEBUG return true #else diff --git a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift index b5f7cfb144..610f56f135 100644 --- a/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityIndicator/FullscreenActivityIndicatorPresenter.swift @@ -29,18 +29,18 @@ import UIKit private weak var viewController: UIViewController? private var indicator: UserIndicator? - init(label: String, on viewController: UIViewController) { + init(label: String, viewController: UIViewController) { self.label = label self.viewController = viewController } func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) { - guard let vc = viewController else { + guard let viewController = viewController else { return } let request = UserIndicatorRequest( - presenter: FullscreenLoadingIndicatorPresenter(label: label, on: vc), + presenter: FullscreenLoadingIndicatorPresenter(label: label, viewController: viewController), dismissal: .manual ) diff --git a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift index 8ca7fb026f..da480cbb59 100644 --- a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift @@ -25,18 +25,18 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { private weak var viewController: UIViewController? private weak var view: UIView? - init(label: String, on viewController: UIViewController) { + init(label: String, viewController: UIViewController) { self.label = label self.viewController = viewController } func present() { // Find the current top navigation controller - var vc: UIViewController? = viewController - while vc?.navigationController != nil { - vc = vc?.navigationController + var presentingController: UIViewController? = viewController + while presentingController?.navigationController != nil { + presentingController = presentingController?.navigationController } - guard let presentingVC = vc else { + guard let presentingController = presentingController else { return } @@ -45,12 +45,12 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { self.view = view view.translatesAutoresizingMaskIntoConstraints = false - presentingVC.view.addSubview(view) + presentingController.view.addSubview(view) NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: presentingVC.view.topAnchor), - view.bottomAnchor.constraint(equalTo: presentingVC.view.bottomAnchor), - view.leadingAnchor.constraint(equalTo: presentingVC.view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: presentingVC.view.trailingAnchor) + view.topAnchor.constraint(equalTo: presentingController.view.topAnchor), + view.bottomAnchor.constraint(equalTo: presentingController.view.bottomAnchor), + view.leadingAnchor.constraint(equalTo: presentingController.view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: presentingController.view.trailingAnchor) ]) view.alpha = 0 diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 030b7606ed..4f9cd8c471 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -116,10 +116,10 @@ final class RoomInfoListViewController: UIViewController { // Do any additional setup after loading the view. self.setupViews() - if BuildSettings.appActivityIndicators { + if BuildSettings.useAppUserIndicators { self.activityPresenter = FullscreenActivityIndicatorPresenter( label: VectorL10n.roomParticipantsLeaveProcessing, - on: self + viewController: self ) } else { self.activityPresenter = ActivityIndicatorPresenter() diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 4fd95c2512..6342ccda56 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -76,10 +76,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.activityIndicatorPresenter = ActivityIndicatorPresenter() } - deinit { - indicators.cancelAll() - } - // MARK: - Public methods func start() { @@ -234,7 +230,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { homeViewController.tabBarItem.image = homeViewController.tabBarItem.image homeViewController.accessibilityLabel = VectorL10n.titleHome - if BuildSettings.appActivityIndicators { + if BuildSettings.useAppUserIndicators { homeViewController.userIndicatorPresenter = AppUserIndicatorPresenter(appNavigator: parameters.appNavigator) } @@ -709,7 +705,7 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { // For the moment when a room is left, reset the split detail with placeholder self.resetSplitViewDetails() - if BuildSettings.appActivityIndicators { + if BuildSettings.useAppUserIndicators { parameters.appNavigator .addUserIndicator(.success(VectorL10n.roomParticipantsLeaveSuccess)) .store(in: &indicators) From 6aaccedb1997ac44563fed98d888600bfd1df5fc Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 22 Feb 2022 16:52:48 +0000 Subject: [PATCH 6/6] Revert presenter dismissal --- .../FullscreenLoadingIndicatorPresenter.swift | 15 ++++++++++----- .../ToastUserIndicatorPresenter.swift | 18 +++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift index da480cbb59..e5488a19a7 100644 --- a/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/FullscreenLoadingIndicatorPresenter.swift @@ -54,7 +54,6 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { ]) view.alpha = 0 - CATransaction.commit() UIView.animate(withDuration: 0.2) { view.alpha = 1 } @@ -65,10 +64,16 @@ class FullscreenLoadingIndicatorPresenter: UserIndicatorPresentable { return } - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - } completion: { _ in - view.removeFromSuperview() + // 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() + } } } } diff --git a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift index 281bb92af1..6f7973b3d0 100644 --- a/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift +++ b/Riot/Modules/Common/ActivityPresenters/ToastUserIndicatorPresenter.swift @@ -48,9 +48,7 @@ class ToastUserIndicatorPresenter: UserIndicatorPresentable { ]) view.alpha = 0 - CATransaction.flush() view.transform = .init(translationX: 0, y: 5) - UIView.animate(withDuration: 0.2) { view.alpha = 1 view.transform = .identity @@ -62,11 +60,17 @@ class ToastUserIndicatorPresenter: UserIndicatorPresentable { return } - UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { - view.alpha = 0 - view.transform = .init(translationX: 0, y: -5) - } completion: { _ in - view.removeFromSuperview() + // 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() + } } } }