diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 769e124ad..d802b3557 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -374,6 +374,7 @@ C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C4F784AABFF44E4716E7A8BC /* RoomDetailsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3A76EA6AB67910C11330F /* RoomDetailsViewModelProtocol.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; + C6136E848E55D2C86BF760F5 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */; }; C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C7B251DC896C0867C51B616D /* AnalyticsPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */; }; @@ -915,6 +916,7 @@ C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; @@ -2142,6 +2144,7 @@ 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, + C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */, @@ -3008,6 +3011,7 @@ FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */, 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */, B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */, + C6136E848E55D2C86BF760F5 /* NetworkMonitor.swift in Sources */, 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 77ef2d823..874cb502c 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -47,6 +47,7 @@ class AppCoordinator: AppCoordinatorProtocol { private let bugReportService: BugReportServiceProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol + private var userSessionCancellables = Set() private var cancellables = Set() private(set) var notificationManager: NotificationManagerProtocol? @@ -79,9 +80,8 @@ class AppCoordinator: AppCoordinatorProtocol { Bundle.elementFallbackLanguage = "en" - startObservingApplicationState() - - // Benchmark.trackingEnabled = true + observeApplicationState() + observeNetworkState() } func start() { @@ -101,6 +101,7 @@ class AppCoordinator: AppCoordinatorProtocol { private static func setupServiceLocator(navigationRootCoordinator: NavigationRootCoordinator) { ServiceLocator.shared.register(userNotificationController: UserNotificationController(rootCoordinator: navigationRootCoordinator)) ServiceLocator.shared.register(appSettings: AppSettings()) + ServiceLocator.shared.register(networkMonitor: NetworkMonitor()) } private static func setupLogging() { @@ -329,12 +330,11 @@ class AppCoordinator: AppCoordinatorProtocol { break } } - .store(in: &cancellables) + .store(in: &userSessionCancellables) } private func deobserveUserSessionChanges() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() + userSessionCancellables.removeAll() } // MARK: Toasts and loading indicators @@ -366,7 +366,7 @@ class AppCoordinator: AppCoordinatorProtocol { userSession?.clientProxy.startSync() } - private func startObservingApplicationState() { + private func observeApplicationState() { NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, @@ -401,6 +401,19 @@ class AppCoordinator: AppCoordinatorProtocol { resume() } } + + private func observeNetworkState() { + let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification" + ServiceLocator.shared.networkMonitor.reachabilityPublisher.sink { reachable in + if reachable { + ServiceLocator.shared.userNotificationController.retractNotificationWithId(reachabilityNotificationIdentifier) + } else { + ServiceLocator.shared.userNotificationController.submitNotification(.init(id: reachabilityNotificationIdentifier, + title: ElementL10n.a11yPresenceOffline, + persistent: true)) + } + }.store(in: &cancellables) + } } // MARK: - AuthenticationCoordinatorDelegate diff --git a/ElementX/Sources/Application/ServiceLocator.swift b/ElementX/Sources/Application/ServiceLocator.swift index e9e7f9605..3e1927a45 100644 --- a/ElementX/Sources/Application/ServiceLocator.swift +++ b/ElementX/Sources/Application/ServiceLocator.swift @@ -32,4 +32,10 @@ class ServiceLocator { func register(appSettings: AppSettings) { settings = appSettings } + + private(set) var networkMonitor: NetworkMonitor! + + func register(networkMonitor: NetworkMonitor) { + self.networkMonitor = networkMonitor + } } diff --git a/ElementX/Sources/Other/NetworkMonitor.swift b/ElementX/Sources/Other/NetworkMonitor.swift new file mode 100644 index 000000000..85f53c02b --- /dev/null +++ b/ElementX/Sources/Other/NetworkMonitor.swift @@ -0,0 +1,50 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import Network + +class NetworkMonitor { + private let pathMonitor: NWPathMonitor + private let queue: DispatchQueue + let reachabilityPublisher: CurrentValueSubject + + var isCurrentConnectionExpensive: Bool { + pathMonitor.currentPath.isExpensive + } + + var isCurrentConnectionConstrained: Bool { + pathMonitor.currentPath.isConstrained + } + + init() { + queue = DispatchQueue(label: "io.element.elementx.networkmonitor") + pathMonitor = NWPathMonitor() + reachabilityPublisher = CurrentValueSubject(pathMonitor.currentPath.status == .satisfied) + + pathMonitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + if path.status == .satisfied { + self?.reachabilityPublisher.send(true) + } else { + self?.reachabilityPublisher.send(false) + } + } + } + pathMonitor.start(queue: queue) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 41f403db0..beb2ceb05 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -120,7 +120,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } func stop() { - cancellables.forEach { $0.cancel() } cancellables.removeAll() state.contextMenuBuilder = nil } diff --git a/changelog.d/258.feature b/changelog.d/258.feature new file mode 100644 index 000000000..c070efe99 --- /dev/null +++ b/changelog.d/258.feature @@ -0,0 +1 @@ +Display an indicator if the network is currently unreachable \ No newline at end of file