mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #5630 from vector-im/andy/5605_leave_room
Update activity indicators on leaving room
This commit is contained in:
commit
c7985e5a1a
29 changed files with 603 additions and 228 deletions
|
@ -16,7 +16,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
class ActivityPresenterSpy: ActivityPresentable {
|
||||
class UserIndicatorPresenterSpy: UserIndicatorPresentable {
|
||||
var intel = [String]()
|
||||
|
||||
func present() {
|
|
@ -17,36 +17,36 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
class ActivityCenterTests: XCTestCase {
|
||||
var activities: [Activity]!
|
||||
var center: ActivityCenter!
|
||||
class UserIndicatorQueueTests: XCTestCase {
|
||||
var indicators: [UserIndicator]!
|
||||
var queue: UserIndicatorQueue!
|
||||
|
||||
override func setUp() {
|
||||
activities = []
|
||||
center = ActivityCenter()
|
||||
indicators = []
|
||||
queue = 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 = queue.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() {
|
||||
queue.add(makeRequest()).store(in: &indicators)
|
||||
let indicator = queue.add(makeRequest())
|
||||
XCTAssertEqual(indicator.state, .pending)
|
||||
}
|
||||
|
||||
func testSecondActivityIsExecutingWhenFirstCompleted() {
|
||||
let first = center.add(makeRequest())
|
||||
let second = center.add(makeRequest())
|
||||
func testSecondIndicatorIsExecutingWhenFirstCompleted() {
|
||||
let first = queue.add(makeRequest())
|
||||
let second = queue.add(makeRequest())
|
||||
|
||||
first.cancel()
|
||||
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
enum State {
|
||||
public class UserIndicator {
|
||||
public enum State {
|
||||
case pending
|
||||
case executing
|
||||
case completed
|
||||
}
|
||||
|
||||
private let request: ActivityRequest
|
||||
private let request: UserIndicatorRequest
|
||||
private let completion: () -> Void
|
||||
|
||||
private(set) var state: State
|
||||
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
|
||||
|
||||
|
@ -45,7 +45,7 @@ public class Activity {
|
|||
}
|
||||
|
||||
deinit {
|
||||
cancel()
|
||||
complete()
|
||||
}
|
||||
|
||||
internal func start() {
|
||||
|
@ -66,11 +66,11 @@ 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.
|
||||
func cancel() {
|
||||
/// 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,8 +87,16 @@ public class Activity {
|
|||
}
|
||||
}
|
||||
|
||||
public extension Activity {
|
||||
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == Activity {
|
||||
public extension UserIndicator {
|
||||
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator {
|
||||
collection.append(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Collection where Element == UserIndicator {
|
||||
func cancelAll() {
|
||||
forEach {
|
||||
$0.cancel()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<T: AnyObject> {
|
||||
weak var element: T?
|
||||
init(_ element: T) {
|
||||
|
@ -28,27 +28,27 @@ public class ActivityCenter {
|
|||
}
|
||||
}
|
||||
|
||||
public static let shared = ActivityCenter()
|
||||
private var queue = [Weak<Activity>]()
|
||||
public static let shared = UserIndicatorQueue()
|
||||
private var queue = [Weak<UserIndicator>]()
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -226,7 +226,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 false
|
||||
#else
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
@ -3691,6 +3695,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")
|
||||
|
@ -3707,6 +3715,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)
|
||||
|
|
|
@ -322,15 +322,40 @@ 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
|
||||
)
|
||||
return ActivityCenter.shared.add(request)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,15 @@
|
|||
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
|
||||
|
@ -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 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 addLoadingActivity() -> Activity
|
||||
/// To remove an indicator, cancel or deallocate the returned `UserIndicator`
|
||||
func addUserIndicator(_ type: AppUserIndicatorType) -> UserIndicator
|
||||
}
|
||||
|
|
|
@ -1,47 +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 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.
|
||||
///
|
||||
/// Note: clients can skip using `AppActivityIndicatorPresenter` 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 AppActivityIndicatorPresenter: NSObject, ActivityIndicatorPresenterType {
|
||||
private let appNavigator: AppNavigatorProtocol
|
||||
private var activity: Activity?
|
||||
|
||||
init(appNavigator: AppNavigatorProtocol) {
|
||||
self.appNavigator = appNavigator
|
||||
}
|
||||
|
||||
@objc func presentActivityIndicator() {
|
||||
activity = appNavigator.addLoadingActivity()
|
||||
}
|
||||
|
||||
@objc func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) {
|
||||
activity = nil
|
||||
}
|
||||
|
||||
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) {
|
||||
MXLog.error("[AppActivityIndicatorPresenter] Shared activity indicator does not support presenting from custom views")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
|
@ -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 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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// 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?
|
||||
|
||||
init(label: String, viewController: UIViewController) {
|
||||
self.label = label
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func present() {
|
||||
// Find the current top navigation controller
|
||||
var presentingController: UIViewController? = viewController
|
||||
while presentingController?.navigationController != nil {
|
||||
presentingController = presentingController?.navigationController
|
||||
}
|
||||
guard let presentingController = presentingController else {
|
||||
return
|
||||
}
|
||||
|
||||
let view = LabelledActivityIndicatorView(text: label)
|
||||
view.update(theme: ThemeService.shared().theme)
|
||||
self.view = view
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
presentingController.view.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
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
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
view.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
} completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
/// 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(text: String, navigationController: UINavigationController) {
|
||||
self.text = text
|
||||
init(viewState: ToastViewState, 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
|
||||
|
||||
|
@ -69,7 +70,6 @@ class ActivityIndicatorToastPresenter: ActivityPresentable {
|
|||
view.transform = .init(translationX: 0, y: -5)
|
||||
} completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
self.view = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1280,8 +1280,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
|||
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 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
|||
{
|
||||
typeof(self) self = weakSelf;
|
||||
[self stopActivityIndicator];
|
||||
[self.userIndicatorPresenter presentSuccessWithLabel:[VectorL10n roomParticipantsLeaveSuccess]];
|
||||
// Force table refresh
|
||||
[self cancelEditionMode:YES];
|
||||
}
|
||||
|
@ -2410,20 +2410,28 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
|||
#pragma mark - Activity Indicator
|
||||
|
||||
- (BOOL)providesCustomActivityIndicator {
|
||||
return self.activityPresenter != nil;
|
||||
return self.userIndicatorPresenter != nil;
|
||||
}
|
||||
|
||||
- (void)startActivityIndicatorWithLabel:(NSString *)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];
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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 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)
|
||||
])
|
||||
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: ToastViewState) {
|
||||
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: ToastViewState) {
|
||||
setupLayer()
|
||||
setupStackView()
|
||||
stackView.addArrangedSubview(activityIndicator)
|
||||
stackView.addArrangedSubview(toastView(for: viewState.style))
|
||||
stackView.addArrangedSubview(label)
|
||||
label.text = text
|
||||
label.text = viewState.label
|
||||
}
|
||||
|
||||
private func setupStackView() {
|
||||
|
@ -85,7 +98,19 @@ class ActivityIndicatorToastView: 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
|
||||
}
|
||||
|
||||
private func toastView(for style: ToastViewState.Style) -> UIView {
|
||||
switch style {
|
||||
case .loading:
|
||||
return activityIndicator
|
||||
case .success:
|
||||
imageView.image = Asset.Images.checkmark.image
|
||||
return imageView
|
||||
}
|
||||
}
|
||||
}
|
27
Riot/Modules/Common/Toasts/ToastViewState.swift
Normal file
27
Riot/Modules/Common/Toasts/ToastViewState.swift
Normal file
|
@ -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
|
||||
}
|
|
@ -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.useAppUserIndicators {
|
||||
self.activityPresenter = FullscreenActivityIndicatorPresenter(
|
||||
label: VectorL10n.roomParticipantsLeaveProcessing,
|
||||
viewController: 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) {
|
||||
|
|
|
@ -1066,15 +1066,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
|||
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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
|||
[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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
|||
}];
|
||||
}
|
||||
|
||||
- (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
|
||||
|
@ -7031,14 +7030,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
|||
|
||||
- (void)roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||
{
|
||||
if (self.delegate)
|
||||
{
|
||||
[self.delegate roomViewControllerDidLeaveRoom:self];
|
||||
}
|
||||
else
|
||||
{
|
||||
[[AppDelegate theDelegate] restoreInitialDisplay:nil];
|
||||
}
|
||||
[self notifyDelegateOnLeaveRoomIfNecessary];
|
||||
}
|
||||
|
||||
#pragma mark - RemoveJitsiWidgetViewDelegate
|
||||
|
|
|
@ -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 indicators = [UserIndicator]()
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
|
@ -227,8 +230,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
|||
homeViewController.tabBarItem.image = homeViewController.tabBarItem.image
|
||||
homeViewController.accessibilityLabel = VectorL10n.titleHome
|
||||
|
||||
if BuildSettings.appActivityIndicators {
|
||||
homeViewController.activityPresenter = AppActivityIndicatorPresenter(appNavigator: parameters.appNavigator)
|
||||
if BuildSettings.useAppUserIndicators {
|
||||
homeViewController.userIndicatorPresenter = AppUserIndicatorPresenter(appNavigator: parameters.appNavigator)
|
||||
}
|
||||
|
||||
let wrapperViewController = HomeViewControllerWithBannerWrapperViewController(viewController: homeViewController)
|
||||
|
@ -702,6 +705,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.useAppUserIndicators {
|
||||
parameters.appNavigator
|
||||
.addUserIndicator(.success(VectorL10n.roomParticipantsLeaveSuccess))
|
||||
.store(in: &indicators)
|
||||
}
|
||||
}
|
||||
|
||||
func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) {
|
||||
|
|
1
changelog.d/5605.change
Normal file
1
changelog.d/5605.change
Normal file
|
@ -0,0 +1 @@
|
|||
Activity Indicators: Update loading and success messages when leaving room
|
Loading…
Reference in a new issue