From ab39d8de06b8beee455e27a71d8ebacbf4625456 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 8 Oct 2026 14:30:28 +0300 Subject: [PATCH] Update the app badge and clear read notifications on room list updates The sync update action is sent from the room list service state listener, which fires before the SDK has applied the new unread counts, leaving the app badge one sync behind. Drive both the badge and the clearing of delivered notifications for fully read rooms from the room list publisher instead, moving the observer from the ChatsTabFlowCoordinator into the UserSessionFlowCoordinator. The NotificationManager now receives the badge count from the caller so the same value can be reused elsewhere. --- .../ChatsTabFlowCoordinator.swift | 6 ------ .../UserSessionFlowCoordinator.swift | 17 ++++++++++++++++ .../Mocks/Generated/GeneratedMocks.swift | 20 ++++++++++++++++--- .../Manager/NotificationManager.swift | 4 +--- .../Manager/NotificationManagerProtocol.swift | 2 +- .../NotificationManagerTests.swift | 8 +++----- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift index c8be799ec..9c819da62 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift @@ -337,12 +337,6 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { switch action { case .receivedDecryptionError(let info): processDecryptionError(info) - case .receivedSyncUpdate: - Task { - let roomSummaries = self.userSession.clientProxy.staticRoomSummaryProvider.roomListPublisher.value - await self.flowParameters.notificationManager.removeDeliveredNotificationsForFullyReadRooms(roomSummaries) - await self.flowParameters.notificationManager.updateAppBadgeCount() - } default: break } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index e18b1ad46..e15dc861a 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -317,6 +317,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) setupCallObservers() + setupUnreadNotificationsObserver() searchScreenCoordinator?.actionsPublisher .sink { [weak self] action in @@ -530,6 +531,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationTabCoordinator.setOverlayPresentationMode(.minimized) } + private func setupUnreadNotificationsObserver() { + let roomListPublisher = userSession.clientProxy.staticRoomSummaryProvider.roomListPublisher + + Task { [weak self] in + for await roomSummaries in roomListPublisher.values { + guard let self else { return } + + let badgeCount = Int(userSession.clientProxy.totalUnreadNotifications) + + await flowParameters.notificationManager.removeDeliveredNotificationsForFullyReadRooms(roomSummaries) + await flowParameters.notificationManager.updateAppBadgeCount(badgeCount) + } + } + .store(in: &cancellables) + } + // MARK: - Native calls private func setupCallObservers() { diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index fa6da126c..d0b4a4cef 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -9039,11 +9039,25 @@ nonisolated class NotificationManagerMock: NotificationManagerProtocol, @uncheck var updateAppBadgeCountCalled: Bool { return updateAppBadgeCountCallsCount > 0 } - nonisolated(unsafe) var updateAppBadgeCountClosure: (() async -> Void)? + private let updateAppBadgeCountReceivedBadgeCountLock = NSLock() + private nonisolated(unsafe) var updateAppBadgeCountUnderlyingReceivedBadgeCount: Int? + var updateAppBadgeCountReceivedBadgeCount: Int? { + get { updateAppBadgeCountReceivedBadgeCountLock.withLock { updateAppBadgeCountUnderlyingReceivedBadgeCount } } + set { updateAppBadgeCountReceivedBadgeCountLock.withLock { updateAppBadgeCountUnderlyingReceivedBadgeCount = newValue } } + } + private let updateAppBadgeCountReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var updateAppBadgeCountUnderlyingReceivedInvocations: [Int] = [] + var updateAppBadgeCountReceivedInvocations: [Int] { + get { updateAppBadgeCountReceivedInvocationsLock.withLock { updateAppBadgeCountUnderlyingReceivedInvocations } } + set { updateAppBadgeCountReceivedInvocationsLock.withLock { updateAppBadgeCountUnderlyingReceivedInvocations = newValue } } + } + nonisolated(unsafe) var updateAppBadgeCountClosure: ((Int) async -> Void)? - @concurrent func updateAppBadgeCount() async { + @concurrent func updateAppBadgeCount(_ badgeCount: Int) async { updateAppBadgeCountCallsCountLock.withLock { updateAppBadgeCountUnderlyingCallsCount += 1 } - await updateAppBadgeCountClosure?() + updateAppBadgeCountReceivedBadgeCount = badgeCount + updateAppBadgeCountReceivedInvocationsLock.withLock { updateAppBadgeCountUnderlyingReceivedInvocations.append(badgeCount) } + await updateAppBadgeCountClosure?(badgeCount) } } nonisolated class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol, @unchecked Sendable { diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index d9f939621..c1b3091b7 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -161,11 +161,9 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers) } - func updateAppBadgeCount() async { + func updateAppBadgeCount(_ badgeCount: Int) async { guard let userSession else { return } - let badgeCount = Int(userSession.clientProxy.totalUnreadNotifications) - appSettings.lastKnownBadgeCount = badgeCount MXLog.debug("Updating app badge count to \(badgeCount)") diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift index 98d1f7d59..c62427c8d 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -37,5 +37,5 @@ protocol NotificationManagerProtocol: AnyObject { func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async - func updateAppBadgeCount() async + func updateAppBadgeCount(_ badgeCount: Int) async } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 1ff5b09c7..3650e3532 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -262,10 +262,8 @@ final class NotificationManagerTests { } @Test - func updatingAppBadgeCountUsesTheClientSideCount() async { - clientProxy.totalUnreadNotifications = 7 - - await notificationManager.updateAppBadgeCount() + func updatingAppBadgeCount() async { + await notificationManager.updateAppBadgeCount(7) #expect(notificationCenter.setBadgeCountReceivedNewBadgeCount == 7) #expect(appSettings.lastKnownBadgeCount == 7) @@ -275,7 +273,7 @@ final class NotificationManagerTests { func updatingAppBadgeCountWithoutASessionDoesNothing() async { notificationManager.setUserSession(nil) - await notificationManager.updateAppBadgeCount() + await notificationManager.updateAppBadgeCount(7) #expect(!notificationCenter.setBadgeCountCalled) }