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.
This commit is contained in:
Doug
2025-09-08 13:18:02 +01:00
committed by GitHub
parent 20b57d0ad7
commit 90504d89c8
26 changed files with 152 additions and 127 deletions
@@ -278,6 +278,7 @@
"common_sent" = "Sent"; "common_sent" = "Sent";
"common_sentence_delimiter" = ". "; "common_sentence_delimiter" = ". ";
"common_server_not_supported" = "Server not supported"; "common_server_not_supported" = "Server not supported";
"common_server_unreachable" = "Server unreachable";
"common_server_url" = "Server URL"; "common_server_url" = "Server URL";
"common_settings" = "Settings"; "common_settings" = "Settings";
"common_shared_location" = "Shared location"; "common_shared_location" = "Shared location";
@@ -150,7 +150,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
setupStateMachine() setupStateMachine()
observeApplicationState() observeApplicationState()
observeNetworkState()
observeAppLockChanges() observeAppLockChanges()
registerBackgroundAppRefresh() 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() { private func observeAppLockChanges() {
appLockFlowCoordinator.actions.sink { [weak self] action in appLockFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return } guard let self else { return }
@@ -1046,7 +1027,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
switch state { switch state {
case .loading: 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)) ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: toastIdentifier, type: .toast(progress: .indeterminate), title: L10n.commonSyncing, persistent: true))
} }
case .notLoading: case .notLoading:
@@ -769,7 +769,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
userIndicatorController: flowParameters.userIndicatorController, userIndicatorController: flowParameters.userIndicatorController,
notificationSettings: userSession.clientProxy.notificationSettings, notificationSettings: userSession.clientProxy.notificationSettings,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: flowParameters.appMediator,
appSettings: flowParameters.appSettings) appSettings: flowParameters.appSettings)
let coordinator = RoomDetailsScreenCoordinator(parameters: params) let coordinator = RoomDetailsScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in coordinator.actions.sink { [weak self] action in
@@ -239,6 +239,29 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
.store(in: &cancellables) .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 onboardingFlowCoordinator.actions
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
@@ -507,7 +530,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private func presentSecureBackupLogoutConfirmationScreen() { private func presentSecureBackupLogoutConfirmationScreen() {
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
appMediator: flowParameters.appMediator)) homeserverReachabilityPublisher: userSession.clientProxy.homeserverReachabilityPublisher))
coordinator.actions coordinator.actions
.sink { [weak self] action in .sink { [weak self] action in
+2
View File
@@ -630,6 +630,8 @@ internal enum L10n {
internal static var commonSentenceDelimiter: String { return L10n.tr("Localizable", "common_sentence_delimiter") } internal static var commonSentenceDelimiter: String { return L10n.tr("Localizable", "common_sentence_delimiter") }
/// Server not supported /// Server not supported
internal static var commonServerNotSupported: String { return L10n.tr("Localizable", "common_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 /// Server URL
internal static var commonServerUrl: String { return L10n.tr("Localizable", "common_server_url") } internal static var commonServerUrl: String { return L10n.tr("Localizable", "common_server_url") }
/// Settings /// Settings
+6 -6
View File
@@ -48,14 +48,14 @@ extension ClientProxyMock {
roomDirectorySearchProxyReturnValue = configuration.roomDirectorySearchProxy roomDirectorySearchProxyReturnValue = configuration.roomDirectorySearchProxy
actionsPublisher = PassthroughSubject<ClientProxyAction, Never>().eraseToAnyPublisher() actionsPublisher = PassthroughSubject<ClientProxyAction, Never>().eraseToAnyPublisher()
loadingStatePublisher = CurrentValuePublisher<ClientProxyLoadingState, Never>(.notLoading) loadingStatePublisher = .init(.notLoading)
verificationStatePublisher = CurrentValuePublisher<SessionVerificationState, Never>(.unknown) verificationStatePublisher = .init(.unknown)
homeserverReachabilityPublisher = .init(.reachable)
userAvatarURLPublisher = CurrentValueSubject<URL?, Never>(nil).asCurrentValuePublisher() userAvatarURLPublisher = .init(nil)
userDisplayNamePublisher = .init("User display name")
userDisplayNamePublisher = CurrentValueSubject<String?, Never>("User display name").asCurrentValuePublisher() ignoredUsersPublisher = .init([RoomMemberProxyMock].allMembers.map(\.userID))
ignoredUsersPublisher = CurrentValueSubject<[String]?, Never>([RoomMemberProxyMock].allMembers.map(\.userID)).asCurrentValuePublisher()
notificationSettings = configuration.notificationSettings notificationSettings = configuration.notificationSettings
@@ -2082,6 +2082,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
set(value) { underlyingVerificationStatePublisher = value } set(value) { underlyingVerificationStatePublisher = value }
} }
var underlyingVerificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never>! var underlyingVerificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never>!
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
get { return underlyingHomeserverReachabilityPublisher }
set(value) { underlyingHomeserverReachabilityPublisher = value }
}
var underlyingHomeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>!
var userID: String { var userID: String {
get { return underlyingUserID } get { return underlyingUserID }
set(value) { underlyingUserID = value } set(value) { underlyingUserID = value }
@@ -26,8 +26,10 @@ class NetworkMonitor: NetworkMonitorProtocol {
pathMonitor.pathUpdateHandler = { [weak self] path in pathMonitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async { DispatchQueue.main.async {
if path.status == .satisfied { if path.status == .satisfied {
MXLog.info("Network reachability changed to reachable")
self?.reachabilitySubject.send(.reachable) self?.reachabilitySubject.send(.reachable)
} else { } else {
MXLog.info("Network reachability changed to unreachable")
self?.reachabilitySubject.send(.unreachable) self?.reachabilitySubject.send(.unreachable)
} }
} }
@@ -15,7 +15,6 @@ struct RoomDetailsScreenCoordinatorParameters {
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
let notificationSettings: NotificationSettingsProxyProtocol let notificationSettings: NotificationSettingsProxyProtocol
let attributedStringBuilder: AttributedStringBuilderProtocol let attributedStringBuilder: AttributedStringBuilderProtocol
let appMediator: AppMediatorProtocol
let appSettings: AppSettings let appSettings: AppSettings
} }
@@ -54,7 +53,6 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
userIndicatorController: parameters.userIndicatorController, userIndicatorController: parameters.userIndicatorController,
notificationSettingsProxy: parameters.notificationSettings, notificationSettingsProxy: parameters.notificationSettings,
attributedStringBuilder: parameters.attributedStringBuilder, attributedStringBuilder: parameters.attributedStringBuilder,
appMediator: parameters.appMediator,
appSettings: parameters.appSettings) appSettings: parameters.appSettings)
} }
@@ -49,7 +49,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
notificationSettingsProxy: NotificationSettingsProxyProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol,
attributedStringBuilder: AttributedStringBuilderProtocol, attributedStringBuilder: AttributedStringBuilderProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings) { appSettings: AppSettings) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.userSession = userSession self.userSession = userSession
@@ -79,7 +78,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported
} }
appMediator.networkMonitor.reachabilityPublisher userSession.clientProxy.homeserverReachabilityPublisher
.filter { $0 == .reachable } .filter { $0 == .reachable }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -397,7 +397,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy, notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -427,7 +426,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy, notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -466,7 +464,6 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy, notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
} }
@@ -71,7 +71,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
roomProxy: parameters.roomProxy, roomProxy: parameters.roomProxy,
initialSelectedPinnedEventID: selectedPinnedEventID, initialSelectedPinnedEventID: selectedPinnedEventID,
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher, ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
appMediator: parameters.appMediator,
appSettings: parameters.appSettings, appSettings: parameters.appSettings,
appHooks: parameters.appHooks, appHooks: parameters.appHooks,
analyticsService: parameters.analytics, analyticsService: parameters.analytics,
@@ -16,7 +16,6 @@ typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, Roo
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol private let clientProxy: ClientProxyProtocol
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings private let appSettings: AppSettings
private let analyticsService: AnalyticsService private let analyticsService: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
@@ -54,14 +53,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomProxy: JoinedRoomProxyProtocol, roomProxy: JoinedRoomProxyProtocol,
initialSelectedPinnedEventID: String?, initialSelectedPinnedEventID: String?,
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>, ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
appMediator: AppMediatorProtocol,
appSettings: AppSettings, appSettings: AppSettings,
appHooks: AppHooks, appHooks: AppHooks,
analyticsService: AnalyticsService, analyticsService: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol) {
clientProxy = userSession.clientProxy clientProxy = userSession.clientProxy
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.appMediator = appMediator
self.appSettings = appSettings self.appSettings = appSettings
self.analyticsService = analyticsService self.analyticsService = analyticsService
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
@@ -179,7 +176,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
.store(in: &cancellables) .store(in: &cancellables)
appMediator.networkMonitor.reachabilityPublisher clientProxy.homeserverReachabilityPublisher
.filter { $0 == .reachable } .filter { $0 == .reachable }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -413,13 +410,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
extension RoomScreenViewModel { extension RoomScreenViewModel {
static func mock(roomProxyMock: JoinedRoomProxyMock, static func mock(roomProxyMock: JoinedRoomProxyMock,
clientProxyMock: ClientProxyMock = ClientProxyMock(), clientProxyMock: ClientProxyMock = ClientProxyMock(.init()),
appHooks: AppHooks = AppHooks()) -> RoomScreenViewModel { appHooks: AppHooks = AppHooks()) -> RoomScreenViewModel {
RoomScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), RoomScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: appHooks, appHooks: appHooks,
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -10,7 +10,7 @@ import SwiftUI
struct SecureBackupLogoutConfirmationScreenCoordinatorParameters { struct SecureBackupLogoutConfirmationScreenCoordinatorParameters {
let secureBackupController: SecureBackupControllerProtocol let secureBackupController: SecureBackupControllerProtocol
let appMediator: AppMediatorProtocol let homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>
} }
enum SecureBackupLogoutConfirmationScreenCoordinatorAction { enum SecureBackupLogoutConfirmationScreenCoordinatorAction {
@@ -30,7 +30,7 @@ final class SecureBackupLogoutConfirmationScreenCoordinator: CoordinatorProtocol
init(parameters: SecureBackupLogoutConfirmationScreenCoordinatorParameters) { init(parameters: SecureBackupLogoutConfirmationScreenCoordinatorParameters) {
viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: parameters.secureBackupController, viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: parameters.secureBackupController,
appMediator: parameters.appMediator) homeserverReachabilityPublisher: parameters.homeserverReachabilityPublisher)
} }
func start() { func start() {
@@ -12,7 +12,7 @@ typealias SecureBackupLogoutConfirmationScreenViewModelType = StateStoreViewMode
class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmationScreenViewModelType, SecureBackupLogoutConfirmationScreenViewModelProtocol { class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmationScreenViewModelType, SecureBackupLogoutConfirmationScreenViewModelProtocol {
private let secureBackupController: SecureBackupControllerProtocol private let secureBackupController: SecureBackupControllerProtocol
private let appMediator: AppMediatorProtocol private let homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>
private let backupUploadStateSubject: CurrentValueSubject<SecureBackupSteadyState, Never> = .init(.waiting) private let backupUploadStateSubject: CurrentValueSubject<SecureBackupSteadyState, Never> = .init(.waiting)
@@ -27,13 +27,13 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(secureBackupController: SecureBackupControllerProtocol, appMediator: AppMediatorProtocol) { init(secureBackupController: SecureBackupControllerProtocol, homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>) {
self.secureBackupController = secureBackupController self.secureBackupController = secureBackupController
self.appMediator = appMediator self.homeserverReachabilityPublisher = homeserverReachabilityPublisher
super.init(initialViewState: .init(mode: .saveRecoveryKey)) super.init(initialViewState: .init(mode: .saveRecoveryKey))
backupUploadStateSubject.combineLatest(appMediator.networkMonitor.reachabilityPublisher) backupUploadStateSubject.combineLatest(homeserverReachabilityPublisher)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] backupState, reachability in .sink { [weak self] backupState, reachability in
guard let self, state.mode != .saveRecoveryKey else { return } guard let self, state.mode != .saveRecoveryKey else { return }
@@ -62,7 +62,7 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat
private func attemptLogout() { private func attemptLogout() {
if case .saveRecoveryKey = state.mode { if case .saveRecoveryKey = state.mode {
updateMode(backupState: backupUploadStateSubject.value, reachability: appMediator.networkMonitor.reachabilityPublisher.value) updateMode(backupState: backupUploadStateSubject.value, reachability: homeserverReachabilityPublisher.value)
keyUploadWaitingTask = Task { keyUploadWaitingTask = Task {
var result = await secureBackupController.waitForKeyBackupUpload(uploadStateSubject: backupUploadStateSubject) var result = await secureBackupController.waitForKeyBackupUpload(uploadStateSubject: backupUploadStateSubject)
@@ -161,14 +161,9 @@ struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestableP
} }
let reachability: NetworkMonitorReachability = mode == .offline ? .unreachable : .reachable let reachability: NetworkMonitorReachability = mode == .offline ? .unreachable : .reachable
let networkMonitor = NetworkMonitorMock()
networkMonitor.underlyingReachabilityPublisher = CurrentValueSubject<NetworkMonitorReachability, Never>(reachability).asCurrentValuePublisher()
let appMediator = AppMediatorMock()
appMediator.underlyingNetworkMonitor = networkMonitor
let viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, let viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController,
appMediator: appMediator) homeserverReachabilityPublisher: .init(reachability))
if mode != .saveRecoveryKey { if mode != .saveRecoveryKey {
viewModel.context.send(viewAction: .logout) viewModel.context.send(viewAction: .logout)
@@ -137,6 +137,11 @@ class ClientProxy: ClientProxyProtocol {
verificationStateSubject.asCurrentValuePublisher() verificationStateSubject.asCurrentValuePublisher()
} }
private let homeserverReachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
homeserverReachabilitySubject.asCurrentValuePublisher()
}
private let timelineMediaVisibilitySubject = CurrentValueSubject<TimelineMediaVisibility, Never>(.always) private let timelineMediaVisibilitySubject = CurrentValueSubject<TimelineMediaVisibility, Never>(.always)
var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> { var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> {
timelineMediaVisibilitySubject.asCurrentValuePublisher() timelineMediaVisibilitySubject.asCurrentValuePublisher()
@@ -218,10 +223,10 @@ class ClientProxy: ClientProxyProtocol {
}) })
sendQueueStatusSubject sendQueueStatusSubject
.combineLatest(networkMonitor.reachabilityPublisher) .combineLatest(homeserverReachabilityPublisher)
.debounce(for: 1.0, scheduler: DispatchQueue.main) .debounce(for: 1.0, scheduler: DispatchQueue.main)
.sink { enabled, reachability in .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 { if enabled == false, reachability == .reachable {
MXLog.info("Enabling all send queues") MXLog.info("Enabling all send queues")
@@ -926,12 +931,11 @@ class ClientProxy: ClientProxyProtocol {
switch state { switch state {
case .running, .terminated, .idle: case .running, .terminated, .idle:
break homeserverReachabilitySubject.send(.reachable)
case .offline:
homeserverReachabilitySubject.send(.unreachable)
case .error: case .error:
restartSync() 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 let syncService = try await client
.syncService() .syncService()
.withCrossProcessLock() .withCrossProcessLock()
.withOfflineMode()
.withSharePos(enable: true) .withSharePos(enable: true)
.finish() .finish()
@@ -80,6 +80,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> { get } var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> { get }
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { get }
var userID: String { get } var userID: String { get }
var deviceID: String? { get } var deviceID: String? { get }
@@ -12,14 +12,14 @@ import UIKit
class MediaProvider: MediaProviderProtocol { class MediaProvider: MediaProviderProtocol {
private let mediaLoader: MediaLoaderProtocol private let mediaLoader: MediaLoaderProtocol
private let imageCache: Kingfisher.ImageCache private let imageCache: Kingfisher.ImageCache
private let networkMonitor: NetworkMonitorProtocol? private let homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>?
init(mediaLoader: MediaLoaderProtocol, init(mediaLoader: MediaLoaderProtocol,
imageCache: Kingfisher.ImageCache, imageCache: Kingfisher.ImageCache,
networkMonitor: NetworkMonitorProtocol?) { homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>?) {
self.mediaLoader = mediaLoader self.mediaLoader = mediaLoader
self.imageCache = imageCache self.imageCache = imageCache
self.networkMonitor = networkMonitor self.homeserverReachabilityPublisher = homeserverReachabilityPublisher
} }
// MARK: Images // MARK: Images
@@ -67,8 +67,8 @@ class MediaProvider: MediaProviderProtocol {
} }
func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task<UIImage, any Error> { func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task<UIImage, any Error> {
guard let networkMonitor else { guard let homeserverReachabilityPublisher else {
fatalError("This method shouldn't be invoked without a NetworkMonitor set.") fatalError("This method shouldn't be invoked without a homeserver reachability publisher set.")
} }
return Task { return Task {
@@ -80,7 +80,7 @@ class MediaProvider: MediaProviderProtocol {
throw MediaProviderError.cancelled throw MediaProviderError.cancelled
} }
for await reachability in networkMonitor.reachabilityPublisher.values { for await reachability in homeserverReachabilityPublisher.values {
guard !Task.isCancelled else { guard !Task.isCancelled else {
throw MediaProviderError.cancelled throw MediaProviderError.cancelled
} }
@@ -96,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol {
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol { private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol {
let mediaProvider = MediaProvider(mediaLoader: clientProxy, let mediaProvider = MediaProvider(mediaLoader: clientProxy,
imageCache: .onlyInMemory, imageCache: .onlyInMemory,
networkMonitor: networkMonitor) homeserverReachabilityPublisher: clientProxy.homeserverReachabilityPublisher)
let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider) let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider)
+1 -1
View File
@@ -16,7 +16,7 @@ final class NSEUserSession {
private let userID: String private let userID: String
private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient), private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient),
imageCache: .onlyOnDisk, imageCache: .onlyOnDisk,
networkMonitor: nil) homeserverReachabilityPublisher: nil)
private let delegateHandle: TaskHandle? private let delegateHandle: TaskHandle?
var mediaPreviewVisibility: MediaPreviews { var mediaPreviewVisibility: MediaPreviews {
@@ -15,18 +15,17 @@ import XCTest
final class MediaProviderTests: XCTestCase { final class MediaProviderTests: XCTestCase {
private var mediaLoader: MediaLoaderMock! private var mediaLoader: MediaLoaderMock!
private var imageCache: MockImageCache! private var imageCache: MockImageCache!
private var networkMonitor: NetworkMonitorMock! private var reachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
var mediaProvider: MediaProvider! var mediaProvider: MediaProvider!
override func setUp() { override func setUp() {
mediaLoader = MediaLoaderMock() mediaLoader = MediaLoaderMock()
imageCache = MockImageCache(name: "Test") imageCache = MockImageCache(name: "Test")
networkMonitor = NetworkMonitorMock()
mediaProvider = MediaProvider(mediaLoader: mediaLoader, mediaProvider = MediaProvider(mediaLoader: mediaLoader,
imageCache: imageCache, imageCache: imageCache,
networkMonitor: networkMonitor) homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher())
} }
func testLoadingRetriedOnReconnection() async throws { func testLoadingRetriedOnReconnection() async throws {
@@ -38,20 +37,18 @@ final class MediaProviderTests: XCTestCase {
let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"))
let connectivitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.unreachable) reachabilitySubject.send(.unreachable)
mediaLoader.loadMediaContentForSourceClosure = { _ in mediaLoader.loadMediaContentForSourceClosure = { [reachabilitySubject] _ in
switch connectivitySubject.value { switch reachabilitySubject.value {
case .unreachable: case .unreachable:
connectivitySubject.send(.reachable) reachabilitySubject.send(.reachable)
throw MediaProviderTestsError.error throw MediaProviderTestsError.error
case .reachable: case .reachable:
return pngData return pngData
} }
} }
networkMonitor.underlyingReachabilityPublisher = connectivitySubject.asCurrentValuePublisher()
let result = try? await loadTask.value let result = try? await loadTask.value
XCTAssertNotNil(result) XCTAssertNotNil(result)
@@ -61,12 +58,10 @@ final class MediaProviderTests: XCTestCase {
func testLoadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws { func testLoadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws {
let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"))
let connectivitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable) reachabilitySubject.send(.reachable)
mediaLoader.loadMediaContentForSourceThrowableError = MediaProviderTestsError.error mediaLoader.loadMediaContentForSourceThrowableError = MediaProviderTestsError.error
networkMonitor.underlyingReachabilityPublisher = connectivitySubject.asCurrentValuePublisher()
let result = try? await loadTask.value let result = try? await loadTask.value
XCTAssertNil(result) XCTAssertNil(result)
@@ -32,7 +32,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -45,7 +44,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil }
@@ -65,7 +63,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil }
@@ -86,7 +83,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
context.send(viewAction: .processTapLeave) context.send(viewAction: .processTapLeave)
@@ -139,7 +135,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } let deferred = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil }
@@ -160,7 +155,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil }
@@ -191,7 +185,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil }
@@ -221,7 +214,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil }
@@ -252,7 +244,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil }
@@ -284,7 +275,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -301,7 +291,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -353,7 +342,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -392,7 +380,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -431,7 +418,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -451,7 +437,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -471,7 +456,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
_ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first()
@@ -489,7 +473,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
var deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isError } var deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isError }
@@ -682,7 +665,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState)) { state in let deferred = deferFulfillment(context.observe(\.viewState)) { state in
@@ -704,7 +686,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState)) { state in let deferred = deferFulfillment(context.observe(\.viewState)) { state in
@@ -728,7 +709,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState)) { state in let deferred = deferFulfillment(context.observe(\.viewState)) { state in
@@ -752,7 +732,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock, notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
let deferred = deferFulfillment(context.observe(\.viewState)) { state in let deferred = deferFulfillment(context.observe(\.viewState)) { state in
@@ -42,7 +42,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -122,7 +121,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: "test1", initialSelectedPinnedEventID: "test1",
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -180,7 +178,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -217,7 +214,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(), ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -262,7 +258,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -284,7 +279,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -319,7 +313,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -349,7 +342,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -370,7 +362,6 @@ class RoomScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -23,14 +23,9 @@ class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase {
secureBackupController.underlyingKeyBackupState = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled).asCurrentValuePublisher() secureBackupController.underlyingKeyBackupState = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled).asCurrentValuePublisher()
reachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable) reachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
let networkMonitor = NetworkMonitorMock()
networkMonitor.underlyingReachabilityPublisher = reachabilitySubject.asCurrentValuePublisher()
let appMediator = AppMediatorMock()
appMediator.underlyingNetworkMonitor = networkMonitor
viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController,
appMediator: appMediator) homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher())
} }
func testInitialState() { func testInitialState() {
@@ -12,13 +12,13 @@ import Combine
@MainActor @MainActor
class UserSessionFlowCoordinatorTests: XCTestCase { class UserSessionFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock!
var timelineControllerFactory: TimelineControllerFactoryMock!
var userSessionFlowCoordinator: UserSessionFlowCoordinator! var userSessionFlowCoordinator: UserSessionFlowCoordinator!
var rootCoordinator: NavigationRootCoordinator! var rootCoordinator: NavigationRootCoordinator!
var notificationManager: NotificationManagerMock! var userIndicatorController: UserIndicatorControllerMock!
let stateMachineFactory = PublishedStateMachineFactory() let stateMachineFactory = PublishedStateMachineFactory()
let networkReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
let homeserverReachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never> = .init(.reachable)
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
var tabCoordinator: NavigationTabCoordinator<UserSessionFlowCoordinator.HomeTab>? { rootCoordinator?.rootCoordinator as? NavigationTabCoordinator } var tabCoordinator: NavigationTabCoordinator<UserSessionFlowCoordinator.HomeTab>? { rootCoordinator?.rootCoordinator as? NavigationTabCoordinator }
@@ -28,24 +28,30 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
override func setUp() async throws { override func setUp() async throws {
cancellables.removeAll() cancellables.removeAll()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = TimelineControllerFactoryMock(.init())
rootCoordinator = NavigationRootCoordinator() 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)), let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
bugReportService: BugReportServiceMock(.init()), bugReportService: BugReportServiceMock(.init()),
elementCallService: ElementCallServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()),
timelineControllerFactory: timelineControllerFactory, timelineControllerFactory: TimelineControllerFactoryMock(.init()),
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
appMediator: AppMediatorMock.default, appMediator: appMediator,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(), appHooks: AppHooks(),
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: userIndicatorController,
notificationManager: notificationManager, notificationManager: NotificationManagerMock(),
stateMachineFactory: stateMachineFactory) stateMachineFactory: stateMachineFactory)
userSessionFlowCoordinator = UserSessionFlowCoordinator(isNewLogin: false, userSessionFlowCoordinator = UserSessionFlowCoordinator(isNewLogin: false,
@@ -56,6 +62,8 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
userSessionFlowCoordinator.start() userSessionFlowCoordinator.start()
} }
// MARK: Navigation
func testInitialState() async throws { func testInitialState() async throws {
XCTAssertNotNil(chatsSplitCoordinator) XCTAssertNotNil(chatsSplitCoordinator)
XCTAssertNil(detailCoordinator) XCTAssertNil(detailCoordinator)
@@ -154,6 +162,51 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") 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 // MARK: - Helpers
private func process(route: AppRoute, private func process(route: AppRoute,
@@ -179,4 +232,12 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
try await deferredUserSession?.fulfill() try await deferredUserSession?.fulfill()
try await deferredChatsState?.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
}
} }