Don't show the server unreachable banner while the client is suspended

homeserverReachabilityPublisher now reports unreachable while suspended so media loading parks instead of failing against a closed store. The reachability banner was keyed off the same publisher, so pausing the client would surface a spurious "Server unreachable" message.

Expose a separate homeserverConnectivityPublisher derived from the sync service state alone (it stays reachable while suspended) and key the banner off that, keeping the suspend-aware publisher for gating work that needs an open store.
This commit is contained in:
Stefan Ceriu
2026-06-24 15:49:00 +03:00
parent 05922f1bc7
commit 3ccaa04554
5 changed files with 21 additions and 1 deletions
@@ -285,7 +285,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
.store(in: &cancellables) .store(in: &cancellables)
let reachabilityNotificationID = "io.element.elementx.reachability.notification" let reachabilityNotificationID = "io.element.elementx.reachability.notification"
userSession.clientProxy.homeserverReachabilityPublisher.removeDuplicates() userSession.clientProxy.homeserverConnectivityPublisher.removeDuplicates()
.combineLatest(flowParameters.appMediator.networkMonitor.reachabilityPublisher.removeDuplicates()) .combineLatest(flowParameters.appMediator.networkMonitor.reachabilityPublisher.removeDuplicates())
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] homeserverReachability, networkReachability in .sink { [weak self] homeserverReachability, networkReachability in
@@ -63,6 +63,7 @@ extension ClientProxyMock {
loadingStatePublisher = .init(.notLoading) loadingStatePublisher = .init(.notLoading)
verificationStatePublisher = .init(.unknown) verificationStatePublisher = .init(.unknown)
homeserverReachabilityPublisher = .init(.reachable) homeserverReachabilityPublisher = .init(.reachable)
homeserverConnectivityPublisher = .init(.reachable)
userAvatarURLPublisher = .init(nil) userAvatarURLPublisher = .init(nil)
userDisplayNamePublisher = .init("User display name") userDisplayNamePublisher = .init("User display name")
@@ -2232,6 +2232,11 @@ nonisolated class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
set(value) { underlyingHomeserverReachabilityPublisher = value } set(value) { underlyingHomeserverReachabilityPublisher = value }
} }
nonisolated(unsafe) var underlyingHomeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>! nonisolated(unsafe) var underlyingHomeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>!
var homeserverConnectivityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
get { return underlyingHomeserverConnectivityPublisher }
set(value) { underlyingHomeserverConnectivityPublisher = value }
}
nonisolated(unsafe) var underlyingHomeserverConnectivityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>!
var userID: String { var userID: String {
get { return underlyingUserID } get { return underlyingUserID }
set(value) { underlyingUserID = value } set(value) { underlyingUserID = value }
@@ -180,11 +180,21 @@ class ClientProxy: ClientProxyProtocol {
verificationStateSubject.asCurrentValuePublisher() verificationStateSubject.asCurrentValuePublisher()
} }
/// Reports unreachable while suspended (the store is closed) as well as when offline. Use this to gate
/// work that needs an open store, such as media loading.
private let homeserverReachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable) private let homeserverReachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
homeserverReachabilitySubject.asCurrentValuePublisher() homeserverReachabilitySubject.asCurrentValuePublisher()
} }
/// The homeserver's reachability derived from the sync service alone, i.e. it stays reachable while the
/// client is suspended. Use this for user-facing messaging so we don't claim the server is unreachable
/// when we've simply paused syncing.
private let homeserverConnectivitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
var homeserverConnectivityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
homeserverConnectivitySubject.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()
@@ -1200,6 +1210,8 @@ class ClientProxy: ClientProxyProtocol {
} }
private func updateHomeserverReachability() { private func updateHomeserverReachability() {
homeserverConnectivitySubject.send(syncServiceState == .offline ? .unreachable : .reachable)
let reachability: NetworkMonitorReachability = if desiredServiceState == .running, syncServiceState != .offline { let reachability: NetworkMonitorReachability = if desiredServiceState == .running, syncServiceState != .offline {
.reachable .reachable
} else { } else {
@@ -110,6 +110,8 @@ protocol ClientProxyProtocol: AnyObject {
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { get } var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { get }
var homeserverConnectivityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> { get }
var userID: String { get } var userID: String { get }
var deviceID: String? { get } var deviceID: String? { get }