From 2b65844ac6b42d7ba174b45b2839cace42c9ad13 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:18:02 +0100 Subject: [PATCH] Use the SDK's offline detection everywhere (except for restarting the sync loop). (#4472) * Use the SDK's offline state to drive the offline indicator. * Only use network reachability for restarting the sync loop, use the homeserver reachability for requests. * Add a separate indicator to distinguish when the server is unreachable but the device is online. --- .../en.lproj/Localizable.strings | 1 + .../Sources/Application/AppCoordinator.swift | 21 +---- .../RoomFlowCoordinator.swift | 1 - .../UserSessionFlowCoordinator.swift | 25 +++++- ElementX/Sources/Generated/Strings.swift | 2 + ElementX/Sources/Mocks/ClientProxyMock.swift | 12 +-- .../Mocks/Generated/GeneratedMocks.swift | 5 ++ .../Other/NetworkMonitor/NetworkMonitor.swift | 2 + .../RoomDetailsScreenCoordinator.swift | 2 - .../RoomDetailsScreenViewModel.swift | 3 +- .../View/RoomDetailsScreen.swift | 3 - .../RoomScreen/RoomScreenCoordinator.swift | 1 - .../RoomScreen/RoomScreenViewModel.swift | 8 +- ...pLogoutConfirmationScreenCoordinator.swift | 4 +- ...kupLogoutConfirmationScreenViewModel.swift | 10 +-- ...SecureBackupLogoutConfirmationScreen.swift | 7 +- .../Sources/Services/Client/ClientProxy.swift | 17 ++-- .../Services/Client/ClientProxyProtocol.swift | 2 + .../Media/Provider/MediaProvider.swift | 12 +-- .../UserSession/UserSessionStore.swift | 2 +- NSE/Sources/NSEUserSession.swift | 2 +- .../MediaProvider/MediaProviderTests.swift | 19 ++--- .../RoomDetailsScreenViewModelTests.swift | 21 ----- .../Sources/RoomScreenViewModelTests.swift | 9 --- ...goutConfirmationScreenViewModelTests.swift | 7 +- .../UserSessionFlowCoordinatorTests.swift | 81 ++++++++++++++++--- 26 files changed, 152 insertions(+), 127 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 27a123881..d1c135e27 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -278,6 +278,7 @@ "common_sent" = "Sent"; "common_sentence_delimiter" = ". "; "common_server_not_supported" = "Server not supported"; +"common_server_unreachable" = "Server unreachable"; "common_server_url" = "Server URL"; "common_settings" = "Settings"; "common_shared_location" = "Shared location"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 0bd2d8402..d88aa173a 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -150,7 +150,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg setupStateMachine() observeApplicationState() - observeNetworkState() observeAppLockChanges() registerBackgroundAppRefresh() @@ -826,24 +825,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } } - private func observeNetworkState() { - let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification" - appMediator.networkMonitor - .reachabilityPublisher - .sink { reachability in - MXLog.info("Reachability changed to \(reachability)") - - if reachability == .reachable { - ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(reachabilityNotificationIdentifier) - } else { - ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: reachabilityNotificationIdentifier, - title: L10n.commonOffline, - persistent: true)) - } - } - .store(in: &cancellables) - } - private func observeAppLockChanges() { appLockFlowCoordinator.actions.sink { [weak self] action in guard let self else { return } @@ -1046,7 +1027,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg switch state { case .loading: - if self?.appMediator.networkMonitor.reachabilityPublisher.value == .reachable { + if self?.userSession?.clientProxy.homeserverReachabilityPublisher.value == .reachable { ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: toastIdentifier, type: .toast(progress: .indeterminate), title: L10n.commonSyncing, persistent: true)) } case .notLoading: diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index b3fb064ea..dc074dece 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -769,7 +769,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { userIndicatorController: flowParameters.userIndicatorController, notificationSettings: userSession.clientProxy.notificationSettings, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: flowParameters.appMediator, appSettings: flowParameters.appSettings) let coordinator = RoomDetailsScreenCoordinator(parameters: params) coordinator.actions.sink { [weak self] action in diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 9f16b6989..2b5f5bbf1 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -239,6 +239,29 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } .store(in: &cancellables) + let reachabilityNotificationID = "io.element.elementx.reachability.notification" + userSession.clientProxy.homeserverReachabilityPublisher.removeDuplicates() + .combineLatest(flowParameters.appMediator.networkMonitor.reachabilityPublisher.removeDuplicates()) + .receive(on: DispatchQueue.main) + .sink { [weak self] homeserverReachability, networkReachability in + MXLog.info("Homeserver reachability: \(homeserverReachability)") + + guard let self else { return } + switch (homeserverReachability, networkReachability) { + case (.reachable, _): + flowParameters.userIndicatorController.retractIndicatorWithId(reachabilityNotificationID) + case (.unreachable, .unreachable): + flowParameters.userIndicatorController.submitIndicator(.init(id: reachabilityNotificationID, + title: L10n.commonOffline, + persistent: true)) + case (.unreachable, .reachable): + flowParameters.userIndicatorController.submitIndicator(.init(id: reachabilityNotificationID, + title: L10n.commonServerUnreachable, + persistent: true)) + } + } + .store(in: &cancellables) + onboardingFlowCoordinator.actions .sink { [weak self] action in guard let self else { return } @@ -507,7 +530,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private func presentSecureBackupLogoutConfirmationScreen() { let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, - appMediator: flowParameters.appMediator)) + homeserverReachabilityPublisher: userSession.clientProxy.homeserverReachabilityPublisher)) coordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 867f6c7e0..64aa43ade 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -630,6 +630,8 @@ internal enum L10n { internal static var commonSentenceDelimiter: String { return L10n.tr("Localizable", "common_sentence_delimiter") } /// Server not supported internal static var commonServerNotSupported: String { return L10n.tr("Localizable", "common_server_not_supported") } + /// Server unreachable + internal static var commonServerUnreachable: String { return L10n.tr("Localizable", "common_server_unreachable") } /// Server URL internal static var commonServerUrl: String { return L10n.tr("Localizable", "common_server_url") } /// Settings diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 91aa74ee4..e2c9ae69f 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -48,14 +48,14 @@ extension ClientProxyMock { roomDirectorySearchProxyReturnValue = configuration.roomDirectorySearchProxy actionsPublisher = PassthroughSubject().eraseToAnyPublisher() - loadingStatePublisher = CurrentValuePublisher(.notLoading) - verificationStatePublisher = CurrentValuePublisher(.unknown) + loadingStatePublisher = .init(.notLoading) + verificationStatePublisher = .init(.unknown) + homeserverReachabilityPublisher = .init(.reachable) - userAvatarURLPublisher = CurrentValueSubject(nil).asCurrentValuePublisher() + userAvatarURLPublisher = .init(nil) + userDisplayNamePublisher = .init("User display name") - userDisplayNamePublisher = CurrentValueSubject("User display name").asCurrentValuePublisher() - - ignoredUsersPublisher = CurrentValueSubject<[String]?, Never>([RoomMemberProxyMock].allMembers.map(\.userID)).asCurrentValuePublisher() + ignoredUsersPublisher = .init([RoomMemberProxyMock].allMembers.map(\.userID)) notificationSettings = configuration.notificationSettings diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index ec760a56b..09fdebe4c 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2082,6 +2082,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { set(value) { underlyingVerificationStatePublisher = value } } var underlyingVerificationStatePublisher: CurrentValuePublisher! + var homeserverReachabilityPublisher: CurrentValuePublisher { + get { return underlyingHomeserverReachabilityPublisher } + set(value) { underlyingHomeserverReachabilityPublisher = value } + } + var underlyingHomeserverReachabilityPublisher: CurrentValuePublisher! var userID: String { get { return underlyingUserID } set(value) { underlyingUserID = value } diff --git a/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift b/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift index 16bd1e1b1..265ed5b73 100644 --- a/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift +++ b/ElementX/Sources/Other/NetworkMonitor/NetworkMonitor.swift @@ -26,8 +26,10 @@ class NetworkMonitor: NetworkMonitorProtocol { pathMonitor.pathUpdateHandler = { [weak self] path in DispatchQueue.main.async { if path.status == .satisfied { + MXLog.info("Network reachability changed to reachable") self?.reachabilitySubject.send(.reachable) } else { + MXLog.info("Network reachability changed to unreachable") self?.reachabilitySubject.send(.unreachable) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index d81b182d7..1571fbaee 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -15,7 +15,6 @@ struct RoomDetailsScreenCoordinatorParameters { let userIndicatorController: UserIndicatorControllerProtocol let notificationSettings: NotificationSettingsProxyProtocol let attributedStringBuilder: AttributedStringBuilderProtocol - let appMediator: AppMediatorProtocol let appSettings: AppSettings } @@ -54,7 +53,6 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { userIndicatorController: parameters.userIndicatorController, notificationSettingsProxy: parameters.notificationSettings, attributedStringBuilder: parameters.attributedStringBuilder, - appMediator: parameters.appMediator, appSettings: parameters.appSettings) } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 36d2d0c87..b19735db3 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -49,7 +49,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr userIndicatorController: UserIndicatorControllerProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol, attributedStringBuilder: AttributedStringBuilderProtocol, - appMediator: AppMediatorProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy self.userSession = userSession @@ -79,7 +78,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported } - appMediator.networkMonitor.reachabilityPublisher + userSession.clientProxy.homeserverReachabilityPublisher .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 3e1dbe8d5..c330e4144 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -397,7 +397,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) } @@ -427,7 +426,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) } @@ -466,7 +464,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 8f1574000..5bccef75c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -71,7 +71,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy, initialSelectedPinnedEventID: selectedPinnedEventID, ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher, - appMediator: parameters.appMediator, appSettings: parameters.appSettings, appHooks: parameters.appHooks, analyticsService: parameters.analytics, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 6421ecdb4..b7ee88e10 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -16,7 +16,6 @@ typealias RoomScreenViewModelType = StateStoreViewModel, - appMediator: AppMediatorProtocol, appSettings: AppSettings, appHooks: AppHooks, analyticsService: AnalyticsService, userIndicatorController: UserIndicatorControllerProtocol) { clientProxy = userSession.clientProxy self.roomProxy = roomProxy - self.appMediator = appMediator self.appSettings = appSettings self.analyticsService = analyticsService self.userIndicatorController = userIndicatorController @@ -179,7 +176,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } .store(in: &cancellables) - appMediator.networkMonitor.reachabilityPublisher + clientProxy.homeserverReachabilityPublisher .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -413,13 +410,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol extension RoomScreenViewModel { static func mock(roomProxyMock: JoinedRoomProxyMock, - clientProxyMock: ClientProxyMock = ClientProxyMock(), + clientProxyMock: ClientProxyMock = ClientProxyMock(.init()), appHooks: AppHooks = AppHooks()) -> RoomScreenViewModel { RoomScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: appHooks, analyticsService: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift index 54157c388..6240de930 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift @@ -10,7 +10,7 @@ import SwiftUI struct SecureBackupLogoutConfirmationScreenCoordinatorParameters { let secureBackupController: SecureBackupControllerProtocol - let appMediator: AppMediatorProtocol + let homeserverReachabilityPublisher: CurrentValuePublisher } enum SecureBackupLogoutConfirmationScreenCoordinatorAction { @@ -30,7 +30,7 @@ final class SecureBackupLogoutConfirmationScreenCoordinator: CoordinatorProtocol init(parameters: SecureBackupLogoutConfirmationScreenCoordinatorParameters) { viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: parameters.secureBackupController, - appMediator: parameters.appMediator) + homeserverReachabilityPublisher: parameters.homeserverReachabilityPublisher) } func start() { diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift index 5123d9ad4..9a6582437 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift @@ -12,7 +12,7 @@ typealias SecureBackupLogoutConfirmationScreenViewModelType = StateStoreViewMode class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmationScreenViewModelType, SecureBackupLogoutConfirmationScreenViewModelProtocol { private let secureBackupController: SecureBackupControllerProtocol - private let appMediator: AppMediatorProtocol + private let homeserverReachabilityPublisher: CurrentValuePublisher private let backupUploadStateSubject: CurrentValueSubject = .init(.waiting) @@ -27,13 +27,13 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat actionsSubject.eraseToAnyPublisher() } - init(secureBackupController: SecureBackupControllerProtocol, appMediator: AppMediatorProtocol) { + init(secureBackupController: SecureBackupControllerProtocol, homeserverReachabilityPublisher: CurrentValuePublisher) { self.secureBackupController = secureBackupController - self.appMediator = appMediator + self.homeserverReachabilityPublisher = homeserverReachabilityPublisher super.init(initialViewState: .init(mode: .saveRecoveryKey)) - backupUploadStateSubject.combineLatest(appMediator.networkMonitor.reachabilityPublisher) + backupUploadStateSubject.combineLatest(homeserverReachabilityPublisher) .receive(on: DispatchQueue.main) .sink { [weak self] backupState, reachability in guard let self, state.mode != .saveRecoveryKey else { return } @@ -62,7 +62,7 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat private func attemptLogout() { if case .saveRecoveryKey = state.mode { - updateMode(backupState: backupUploadStateSubject.value, reachability: appMediator.networkMonitor.reachabilityPublisher.value) + updateMode(backupState: backupUploadStateSubject.value, reachability: homeserverReachabilityPublisher.value) keyUploadWaitingTask = Task { var result = await secureBackupController.waitForKeyBackupUpload(uploadStateSubject: backupUploadStateSubject) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift index c41e3d3ad..2f4a838bb 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift @@ -161,14 +161,9 @@ struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestableP } let reachability: NetworkMonitorReachability = mode == .offline ? .unreachable : .reachable - let networkMonitor = NetworkMonitorMock() - networkMonitor.underlyingReachabilityPublisher = CurrentValueSubject(reachability).asCurrentValuePublisher() - - let appMediator = AppMediatorMock() - appMediator.underlyingNetworkMonitor = networkMonitor let viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, - appMediator: appMediator) + homeserverReachabilityPublisher: .init(reachability)) if mode != .saveRecoveryKey { viewModel.context.send(viewAction: .logout) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index dfb79dd39..0eac9a766 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -137,6 +137,11 @@ class ClientProxy: ClientProxyProtocol { verificationStateSubject.asCurrentValuePublisher() } + private let homeserverReachabilitySubject = CurrentValueSubject(.reachable) + var homeserverReachabilityPublisher: CurrentValuePublisher { + homeserverReachabilitySubject.asCurrentValuePublisher() + } + private let timelineMediaVisibilitySubject = CurrentValueSubject(.always) var timelineMediaVisibilityPublisher: CurrentValuePublisher { timelineMediaVisibilitySubject.asCurrentValuePublisher() @@ -218,10 +223,10 @@ class ClientProxy: ClientProxyProtocol { }) sendQueueStatusSubject - .combineLatest(networkMonitor.reachabilityPublisher) + .combineLatest(homeserverReachabilityPublisher) .debounce(for: 1.0, scheduler: DispatchQueue.main) .sink { enabled, reachability in - MXLog.info("Send queue status changed to enabled: \(enabled), reachability: \(reachability)") + MXLog.info("Send queue status changed to enabled: \(enabled), homeserver reachability: \(reachability)") if enabled == false, reachability == .reachable { MXLog.info("Enabling all send queues") @@ -926,12 +931,11 @@ class ClientProxy: ClientProxyProtocol { switch state { case .running, .terminated, .idle: - break + homeserverReachabilitySubject.send(.reachable) + case .offline: + homeserverReachabilitySubject.send(.unreachable) case .error: restartSync() - case .offline: - // This needs to be enabled in the client builder first to be actually used - break } }) } @@ -1196,6 +1200,7 @@ private struct ClientProxyServices { let syncService = try await client .syncService() .withCrossProcessLock() + .withOfflineMode() .withSharePos(enable: true) .finish() diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index a4bc00157..9f1ddf67f 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -80,6 +80,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var verificationStatePublisher: CurrentValuePublisher { get } + var homeserverReachabilityPublisher: CurrentValuePublisher { get } + var userID: String { get } var deviceID: String? { get } diff --git a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift index 11dd30869..2db1ad263 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift @@ -12,14 +12,14 @@ import UIKit class MediaProvider: MediaProviderProtocol { private let mediaLoader: MediaLoaderProtocol private let imageCache: Kingfisher.ImageCache - private let networkMonitor: NetworkMonitorProtocol? + private let homeserverReachabilityPublisher: CurrentValuePublisher? init(mediaLoader: MediaLoaderProtocol, imageCache: Kingfisher.ImageCache, - networkMonitor: NetworkMonitorProtocol?) { + homeserverReachabilityPublisher: CurrentValuePublisher?) { self.mediaLoader = mediaLoader self.imageCache = imageCache - self.networkMonitor = networkMonitor + self.homeserverReachabilityPublisher = homeserverReachabilityPublisher } // MARK: Images @@ -67,8 +67,8 @@ class MediaProvider: MediaProviderProtocol { } func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task { - guard let networkMonitor else { - fatalError("This method shouldn't be invoked without a NetworkMonitor set.") + guard let homeserverReachabilityPublisher else { + fatalError("This method shouldn't be invoked without a homeserver reachability publisher set.") } return Task { @@ -80,7 +80,7 @@ class MediaProvider: MediaProviderProtocol { throw MediaProviderError.cancelled } - for await reachability in networkMonitor.reachabilityPublisher.values { + for await reachability in homeserverReachabilityPublisher.values { guard !Task.isCancelled else { throw MediaProviderError.cancelled } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 55f2f3f6b..8697b9b31 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -96,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol { private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol { let mediaProvider = MediaProvider(mediaLoader: clientProxy, imageCache: .onlyInMemory, - networkMonitor: networkMonitor) + homeserverReachabilityPublisher: clientProxy.homeserverReachabilityPublisher) let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider) diff --git a/NSE/Sources/NSEUserSession.swift b/NSE/Sources/NSEUserSession.swift index c2b3086c4..ca0d6ac97 100644 --- a/NSE/Sources/NSEUserSession.swift +++ b/NSE/Sources/NSEUserSession.swift @@ -16,7 +16,7 @@ final class NSEUserSession { private let userID: String private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient), imageCache: .onlyOnDisk, - networkMonitor: nil) + homeserverReachabilityPublisher: nil) private let delegateHandle: TaskHandle? var mediaPreviewVisibility: MediaPreviews { diff --git a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift index 5379c4a9c..5f832f1a7 100644 --- a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift @@ -15,18 +15,17 @@ import XCTest final class MediaProviderTests: XCTestCase { private var mediaLoader: MediaLoaderMock! private var imageCache: MockImageCache! - private var networkMonitor: NetworkMonitorMock! + private var reachabilitySubject = CurrentValueSubject(.reachable) var mediaProvider: MediaProvider! override func setUp() { mediaLoader = MediaLoaderMock() imageCache = MockImageCache(name: "Test") - networkMonitor = NetworkMonitorMock() mediaProvider = MediaProvider(mediaLoader: mediaLoader, imageCache: imageCache, - networkMonitor: networkMonitor) + homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher()) } func testLoadingRetriedOnReconnection() async throws { @@ -38,20 +37,18 @@ final class MediaProviderTests: XCTestCase { let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) - let connectivitySubject = CurrentValueSubject(.unreachable) + reachabilitySubject.send(.unreachable) - mediaLoader.loadMediaContentForSourceClosure = { _ in - switch connectivitySubject.value { + mediaLoader.loadMediaContentForSourceClosure = { [reachabilitySubject] _ in + switch reachabilitySubject.value { case .unreachable: - connectivitySubject.send(.reachable) + reachabilitySubject.send(.reachable) throw MediaProviderTestsError.error case .reachable: return pngData } } - networkMonitor.underlyingReachabilityPublisher = connectivitySubject.asCurrentValuePublisher() - let result = try? await loadTask.value XCTAssertNotNil(result) @@ -61,12 +58,10 @@ final class MediaProviderTests: XCTestCase { func testLoadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws { let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) - let connectivitySubject = CurrentValueSubject(.reachable) + reachabilitySubject.send(.reachable) mediaLoader.loadMediaContentForSourceThrowableError = MediaProviderTestsError.error - networkMonitor.underlyingReachabilityPublisher = connectivitySubject.asCurrentValuePublisher() - let result = try? await loadTask.value XCTAssertNil(result) diff --git a/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift index daae2d260..2b099ecd3 100644 --- a/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift @@ -32,7 +32,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) } @@ -45,7 +44,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } @@ -65,7 +63,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } @@ -86,7 +83,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) context.send(viewAction: .processTapLeave) @@ -139,7 +135,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } @@ -160,7 +155,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } @@ -191,7 +185,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } @@ -221,7 +214,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } @@ -252,7 +244,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } @@ -284,7 +275,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -301,7 +291,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -353,7 +342,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -392,7 +380,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -431,7 +418,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -451,7 +437,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -471,7 +456,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() @@ -489,7 +473,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) var deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isError } @@ -682,7 +665,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState)) { state in @@ -704,7 +686,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState)) { state in @@ -728,7 +709,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState)) { state in @@ -752,7 +732,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) let deferred = deferFulfillment(context.observe(\.viewState)) { state in diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index bb8d59575..6c5e5cbf1 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -42,7 +42,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -122,7 +121,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: "test1", ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -180,7 +178,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -217,7 +214,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -262,7 +258,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -284,7 +279,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -319,7 +313,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -349,7 +342,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, @@ -370,7 +362,6 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, ongoingCallRoomIDPublisher: .init(.init(nil)), - appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analyticsService: ServiceLocator.shared.analytics, diff --git a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift index 717e4f663..aa1f91d80 100644 --- a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift @@ -23,14 +23,9 @@ class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase { secureBackupController.underlyingKeyBackupState = CurrentValueSubject(.enabled).asCurrentValuePublisher() reachabilitySubject = CurrentValueSubject(.reachable) - let networkMonitor = NetworkMonitorMock() - networkMonitor.underlyingReachabilityPublisher = reachabilitySubject.asCurrentValuePublisher() - - let appMediator = AppMediatorMock() - appMediator.underlyingNetworkMonitor = networkMonitor viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, - appMediator: appMediator) + homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher()) } func testInitialState() { diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 6f4e061e2..11cb10094 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -12,13 +12,13 @@ import Combine @MainActor class UserSessionFlowCoordinatorTests: XCTestCase { - var clientProxy: ClientProxyMock! - var timelineControllerFactory: TimelineControllerFactoryMock! var userSessionFlowCoordinator: UserSessionFlowCoordinator! var rootCoordinator: NavigationRootCoordinator! - var notificationManager: NotificationManagerMock! + var userIndicatorController: UserIndicatorControllerMock! let stateMachineFactory = PublishedStateMachineFactory() + let networkReachabilitySubject: CurrentValueSubject = .init(.reachable) + let homeserverReachabilitySubject: CurrentValueSubject = .init(.reachable) var cancellables = Set() var tabCoordinator: NavigationTabCoordinator? { rootCoordinator?.rootCoordinator as? NavigationTabCoordinator } @@ -28,24 +28,30 @@ class UserSessionFlowCoordinatorTests: XCTestCase { override func setUp() async throws { cancellables.removeAll() - clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) - timelineControllerFactory = TimelineControllerFactoryMock(.init()) rootCoordinator = NavigationRootCoordinator() - notificationManager = NotificationManagerMock() + let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) + clientProxy.homeserverReachabilityPublisher = homeserverReachabilitySubject.asCurrentValuePublisher() + + let networkMonitor = NetworkMonitorMock.default + networkMonitor.reachabilityPublisher = networkReachabilitySubject.asCurrentValuePublisher() + let appMediator = AppMediatorMock.default + appMediator.networkMonitor = networkMonitor + + userIndicatorController = UserIndicatorControllerMock() let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), - timelineControllerFactory: timelineControllerFactory, + timelineControllerFactory: TimelineControllerFactoryMock(.init()), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), - appMediator: AppMediatorMock.default, + appMediator: appMediator, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), analytics: ServiceLocator.shared.analytics, - userIndicatorController: UserIndicatorControllerMock(), - notificationManager: notificationManager, + userIndicatorController: userIndicatorController, + notificationManager: NotificationManagerMock(), stateMachineFactory: stateMachineFactory) userSessionFlowCoordinator = UserSessionFlowCoordinator(isNewLogin: false, @@ -56,6 +62,8 @@ class UserSessionFlowCoordinatorTests: XCTestCase { userSessionFlowCoordinator.start() } + // MARK: Navigation + func testInitialState() async throws { XCTAssertNotNil(chatsSplitCoordinator) XCTAssertNil(detailCoordinator) @@ -154,6 +162,51 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") } + // MARK: Indicators + + func testReachabilityIndicators() async throws { + // Given a flow in its initial state. + try await Task.sleep(for: .milliseconds(100)) + + // Then no reachability indicators should be shown. + XCTAssertFalse(userIndicatorController.submitIndicatorDelayCalled) + XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) // The initial state removes the indicator. + + // When the homeserver becomes unreachable. + homeserverReachabilitySubject.send(.unreachable) + try await Task.sleep(for: .milliseconds(100)) + + // Then a server unreachable indicator should be shown. + XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) + XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonServerUnreachable) + XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) + + // When the network also becomes unreachable. + networkReachabilitySubject.send(.unreachable) + try await Task.sleep(for: .milliseconds(100)) + + // Then the server unreachable indicator should be replaced with an offline indicator. + XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2) + XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonOffline) + XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) + + // When the homeserver becomes reachable again. + homeserverReachabilitySubject.send(.reachable) + try await Task.sleep(for: .milliseconds(100)) + + // Then the indicator should be hidden even if the network isn't reachable. + XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2) + XCTAssertEqual(retractReachabilityIndicatorCallsCount, 2) + + // When the network becomes reachable again. + networkReachabilitySubject.send(.reachable) + try await Task.sleep(for: .milliseconds(100)) + + // Then nothing else should happen. + XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2) + XCTAssertEqual(retractReachabilityIndicatorCallsCount, 3) + } + // MARK: - Helpers private func process(route: AppRoute, @@ -179,4 +232,12 @@ class UserSessionFlowCoordinatorTests: XCTestCase { try await deferredUserSession?.fulfill() try await deferredChatsState?.fulfill() } + + /// Other services retract indicators, so this filters based on the reachability ID. + private var retractReachabilityIndicatorCallsCount: Int { + userIndicatorController + .retractIndicatorWithIdReceivedInvocations + .filter { $0 == "io.element.elementx.reachability.notification" } + .count + } }