Remove the global UserIndicatorController.alertInfo, replacing it with local alertInfo usage. (#5087)

This commit is contained in:
Doug
2026-02-10 17:16:11 +00:00
committed by GitHub
parent 334f4903e4
commit 6dc66ec777
21 changed files with 101 additions and 71 deletions

View File

@@ -226,12 +226,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
guard let confirmationParameters = url.confirmationParameters else {
return false
}
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.dialogConfirmLinkTitle,
message: L10n.dialogConfirmLinkMessage(confirmationParameters.displayString,
confirmationParameters.internalURL.absoluteString),
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionContinue) { openURLAction(confirmationParameters.internalURL) })
navigationRootCoordinator.alertInfo = .init(id: .init(),
title: L10n.dialogConfirmLinkTitle,
message: L10n.dialogConfirmLinkMessage(confirmationParameters.displayString,
confirmationParameters.internalURL.absoluteString),
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionContinue) { openURLAction(confirmationParameters.internalURL) })
return true
}

View File

@@ -67,6 +67,9 @@ import SwiftUI
overlayModule?.coordinator
}
/// The lowest-level `AlertInfo`, directly available to the root of the app.
var alertInfo: AlertInfo<UUID>?
/// Sets or replaces the presented coordinator
/// - Parameter coordinator: the coordinator to display
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) {
@@ -159,6 +162,7 @@ private struct NavigationRootCoordinatorView: View {
ZStack {
rootCoordinator.rootModule?.coordinator?.toPresentable()
}
.alert(item: $rootCoordinator.alertInfo)
.animation(.elementDefault, value: rootCoordinator.rootModule)
.sheet(item: $rootCoordinator.sheetModule) { module in
module.coordinator?.toPresentable()

View File

@@ -485,7 +485,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
let secureBackupController = userSession.clientProxy.secureBackupController
guard case let .success(isLastDevice) = await userSession.clientProxy.isOnlyDeviceLeft() else {
flowParameters.userIndicatorController.alertInfo = .init(id: .init())
navigationRootCoordinator.alertInfo = .init(id: .init())
return
}
@@ -495,26 +495,26 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
guard secureBackupController.recoveryState.value == .enabled else {
flowParameters.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutRecoveryDisabledTitle,
message: L10n.screenSignoutRecoveryDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsTabFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
navigationRootCoordinator.alertInfo = .init(id: .init(),
title: L10n.screenSignoutRecoveryDisabledTitle,
message: L10n.screenSignoutRecoveryDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsTabFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
guard secureBackupController.keyBackupState.value == .enabled else {
flowParameters.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutKeyBackupDisabledTitle,
message: L10n.screenSignoutKeyBackupDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsTabFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
navigationRootCoordinator.alertInfo = .init(id: .init(),
title: L10n.screenSignoutKeyBackupDisabledTitle,
message: L10n.screenSignoutKeyBackupDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsTabFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
@@ -522,12 +522,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
private func logout() {
flowParameters.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutConfirmationDialogTitle,
message: L10n.screenSignoutConfirmationDialogContent,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
})
navigationRootCoordinator.alertInfo = .init(id: .init(),
title: L10n.screenSignoutConfirmationDialogTitle,
message: L10n.screenSignoutConfirmationDialogContent,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
})
}
private func presentSecureBackupLogoutConfirmationScreen() {

View File

@@ -19524,7 +19524,6 @@ class UserIdentityProxyMock: UserIdentityProxyProtocol, @unchecked Sendable {
}
class UserIndicatorControllerMock: UserIndicatorControllerProtocol, @unchecked Sendable {
var window: UIWindow?
var alertInfo: AlertInfo<UUID>?
//MARK: - submitIndicator

View File

@@ -32,8 +32,6 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
}
}
@Published var alertInfo: AlertInfo<UUID>?
var window: UIWindow? {
didSet {
let hostingController = UIHostingController(rootView: UserIndicatorPresenter(userIndicatorController: self).statusBarHidden(ProcessInfo.isRunningUITests))

View File

@@ -15,7 +15,6 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol {
func retractAllIndicators()
var window: UIWindow? { get set }
var alertInfo: AlertInfo<UUID>? { get set }
}
extension UserIndicatorControllerProtocol {

View File

@@ -28,6 +28,5 @@ struct UserIndicatorPresenter: View {
}
}
}
.alert(item: $userIndicatorController.alertInfo)
}
}

View File

@@ -59,7 +59,7 @@ class OIDCAuthenticationPresenter: NSObject {
let errorDescription = error.map(String.init(describing:)) ?? "Unknown error"
MXLog.error("Missing callback URL from the web authentication session: \(errorDescription)")
userIndicatorController.alertInfo = AlertInfo(id: UUID())
showFailureIndicator()
await authenticationService.abortOIDCLogin(data: oidcData)
return .failure(.oidcError(.unknown))
}
@@ -76,7 +76,7 @@ class OIDCAuthenticationPresenter: NSObject {
return .failure(.oidcError(.userCancellation))
case .failure(let error):
MXLog.error("Error occurred: \(error)")
userIndicatorController.alertInfo = AlertInfo(id: UUID())
showFailureIndicator()
return .failure(error)
}
}
@@ -85,10 +85,16 @@ class OIDCAuthenticationPresenter: NSObject {
activeSession?.cancel()
}
private static let loadingIndicatorID = "\(OIDCAuthenticationPresenter.self)-Loading"
private var loadingIndicatorID: String {
"\(Self.self)-Loading"
}
private var failureIndicatorID: String {
"\(Self.self)-Failure"
}
private func startLoading(delay: Duration? = nil) {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorID,
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorID,
type: .modal,
title: L10n.commonLoading,
persistent: true),
@@ -96,7 +102,14 @@ class OIDCAuthenticationPresenter: NSObject {
}
private func stopLoading() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorID)
userIndicatorController.retractIndicatorWithId(loadingIndicatorID)
}
private func showFailureIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: failureIndicatorID,
type: .toast,
title: L10n.errorUnknown,
iconName: "xmark"))
}
}

View File

@@ -10,6 +10,7 @@ import SwiftUI
enum CreateRoomScreenErrorType: Error {
case failedCreatingRoom
case failedProcessingMedia
case failedUploadingMedia
case fileTooLarge
case mediaFileError

View File

@@ -116,7 +116,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
do {
guard case let .success(maxUploadSize) = await userSession.clientProxy.maxMediaUploadSize else {
MXLog.error("Failed to get max upload size")
userIndicatorController.alertInfo = AlertInfo(id: .init())
state.bindings.alertInfo = .init(id: .unknown)
return
}
let mediaInfo = try await mediaUploadingPreprocessor.processMedia(at: fileURL, maxUploadSize: maxUploadSize).get()
@@ -128,7 +128,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
break
}
} catch {
userIndicatorController.alertInfo = AlertInfo(id: .init())
state.bindings.alertInfo = .init(id: .failedProcessingMedia)
}
hideLoadingIndicator()
}

View File

@@ -105,9 +105,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
return
}
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
state.bindings.alertInfo = .init(id: .unknown,
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
}
}

View File

@@ -69,7 +69,10 @@ struct RoomDetailsEditScreenViewStateBindings {
}
enum RoomDetailsEditScreenAlertType {
case failedProcessingMedia
case unsavedChanges
case saveError
case unknown
}
enum RoomDetailsEditScreenViewAction {

View File

@@ -86,7 +86,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
guard case let .success(maxUploadSize) = await clientProxy.maxMediaUploadSize else {
MXLog.error("Failed to get max upload size")
userIndicatorController.alertInfo = .init(id: .init())
state.bindings.alertInfo = .init(id: .unknown)
return
}
let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize)
@@ -95,7 +95,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
case .success(.image):
state.localMedia = try? mediaResult.get()
case .failure, .success:
userIndicatorController.alertInfo = .init(id: .init())
state.bindings.alertInfo = .init(id: .failedProcessingMedia)
}
}
}
@@ -161,9 +161,9 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
actionsSubject.send(.saveFinished)
} catch {
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenRoomDetailsEditionErrorTitle,
message: L10n.screenRoomDetailsEditionError)
state.bindings.alertInfo = .init(id: .saveError,
title: L10n.screenRoomDetailsEditionErrorTitle,
message: L10n.screenRoomDetailsEditionError)
}
}
}

View File

@@ -92,6 +92,11 @@ struct RoomScreenViewState: BindableState {
struct RoomScreenViewStateBindings {
/// The view model used to present a QuickLook media preview.
var mediaPreviewViewModel: TimelineMediaPreviewViewModel?
var alertInfo: AlertInfo<RoomScreenAlertType>?
}
enum RoomScreenAlertType {
case unknown
}
enum RoomScreenFooterViewAction {

View File

@@ -286,7 +286,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
showLoadingIndicator()
if case .failure = await clientProxy.pinUserIdentity(userID) {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
state.bindings.alertInfo = .init(id: .unknown, title: L10n.commonError)
}
}
@@ -298,7 +298,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
showLoadingIndicator()
if case .failure = await clientProxy.withdrawUserIdentityVerification(userID) {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
state.bindings.alertInfo = .init(id: .unknown, title: L10n.commonError)
}
}

View File

@@ -67,6 +67,7 @@ struct RoomScreen: View {
.toolbar { toolbar }
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
.overlay { loadingIndicator }
.alert(item: $context.alertInfo)
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
.track(screen: .Room)
.sentryTrace("\(Self.self)")

View File

@@ -52,7 +52,10 @@ struct UserDetailsEditScreenViewStateBindings {
}
enum UserDetailsEditScreenAlertType {
case failedProcessingMedia
case unsavedChanges
case saveError
case unknown
}
enum UserDetailsEditScreenViewAction {

View File

@@ -92,7 +92,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
guard case let .success(maxUploadSize) = await clientProxy.maxMediaUploadSize else {
MXLog.error("Failed to get max upload size")
userIndicatorController.alertInfo = .init(id: .init())
state.bindings.alertInfo = .init(id: .unknown)
return
}
let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize)
@@ -101,7 +101,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
case .success(.image):
state.localMedia = try? mediaResult.get()
case .failure, .success:
userIndicatorController.alertInfo = .init(id: .init())
state.bindings.alertInfo = .init(id: .failedProcessingMedia)
}
}
}
@@ -149,9 +149,9 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
actionsSubject.send(.dismiss)
} catch {
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenEditProfileErrorTitle,
message: L10n.screenEditProfileError)
state.bindings.alertInfo = .init(id: .saveError,
title: L10n.screenEditProfileErrorTitle,
message: L10n.screenEditProfileError)
}
}
}

View File

@@ -199,6 +199,9 @@ enum TimelineAlertInfoType: Hashable {
case sendingFailed
case encryptionAuthenticity(String)
case encryptionForwarder(String)
case inviteAgain
case unableToInvite
case unknown
}
struct RoomMemberState {

View File

@@ -579,7 +579,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
shouldShowInviteAlert
.sink { [weak self] _ in
self?.showInviteAlert()
self?.displayAlert(.inviteAgain)
}
.store(in: &cancellables)
}
@@ -865,19 +865,11 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Direct chats logics
private func showInviteAlert() {
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenRoomInviteAgainAlertTitle,
message: L10n.screenRoomInviteAgainAlertMessage,
primaryButton: .init(title: L10n.actionInvite) { [weak self] in self?.inviteOtherDMUserBack() },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
private let inviteLoadingIndicatorID = UUID().uuidString
private func inviteOtherDMUserBack() {
guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
displayAlert(.unknown)
return
}
@@ -892,7 +884,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
members.count == 2,
let otherPerson = members.first(where: { $0.userID != roomProxy.ownUserID && $0.membership == .leave })
else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
displayAlert(.unknown)
return
}
@@ -900,9 +892,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .success:
break
case .failure:
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
displayAlert(.unableToInvite)
}
}
}
@@ -1021,6 +1011,18 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
guard let self else { return }
appMediator.open(appSettings.historySharingDetailsURL)
})
case .inviteAgain:
state.bindings.alertInfo = .init(id: .inviteAgain,
title: L10n.screenRoomInviteAgainAlertTitle,
message: L10n.screenRoomInviteAgainAlertMessage,
primaryButton: .init(title: L10n.actionInvite) { [weak self] in self?.inviteOtherDMUserBack() },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
case .unableToInvite:
state.bindings.alertInfo = .init(id: .unableToInvite,
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
case .unknown:
state.bindings.alertInfo = .init(id: .unknown, title: L10n.commonError)
}
}