diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index d2ef9b2fc..af80a22c3 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; + 02F4FAE40AF63A1941FD3BBA /* NotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10B7F8EE25775DE2A305CBB5 /* NotificationCenterProtocol.swift */; }; 037006FB6DF1374F94E4058D /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */; }; 03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26747B3154A5DBC3A7E24A5 /* Image.swift */; }; 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; }; @@ -566,6 +567,7 @@ C413D36D44F89DE63D3ADFA4 /* ReportContentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433BE28B40D418237BE37B5 /* ReportContentScreen.swift */; }; C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C49FCC766673006B6D299F1C /* RoomDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */; }; + C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4151163F666ED94FD959475A /* NotificationName.swift */; }; C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */; }; C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; @@ -835,6 +837,7 @@ 0FB782CE6176A5D2C082EC5D /* WelcomeScreenScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenModels.swift; sourceTree = ""; }; 1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = ""; }; + 10B7F8EE25775DE2A305CBB5 /* NotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterProtocol.swift; sourceTree = ""; }; 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = ""; }; 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenSelectedItem.swift; sourceTree = ""; }; 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -964,6 +967,7 @@ 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenUITests.swift; sourceTree = ""; }; + 4151163F666ED94FD959475A /* NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = ""; }; 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = ""; }; 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2052,6 +2056,7 @@ C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */, 2AFEF3AC64B1358083F76B8B /* List.swift */, 23AA3F4B285570805CB0CCDD /* MapTiler.swift */, + 4151163F666ED94FD959475A /* NotificationName.swift */, F72EFC8C634469F9262659C7 /* NSItemProvider.swift */, 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */, 7310D8DFE01AF45F0689C3AA /* Publisher.swift */, @@ -3194,6 +3199,7 @@ 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */, + 10B7F8EE25775DE2A305CBB5 /* NotificationCenterProtocol.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */, @@ -4386,10 +4392,12 @@ 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */, 9408CE8B8865C0C8DD4C9869 /* NoticeRoomTimelineItemContent.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, + 02F4FAE40AF63A1941FD3BBA /* NotificationCenterProtocol.swift in Sources */, 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */, 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */, + C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */, 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */, F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */, D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 07b2a96ec..bc4844e69 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -197,6 +197,25 @@ class BugReportServiceMock: BugReportServiceProtocol { } } } +class NotificationCenterMock: NotificationCenterProtocol { + + //MARK: - post + + var postNameObjectCallsCount = 0 + var postNameObjectCalled: Bool { + return postNameObjectCallsCount > 0 + } + var postNameObjectReceivedArguments: (aName: NSNotification.Name, anObject: Any?)? + var postNameObjectReceivedInvocations: [(aName: NSNotification.Name, anObject: Any?)] = [] + var postNameObjectClosure: ((NSNotification.Name, Any?) -> Void)? + + func post(name aName: NSNotification.Name, object anObject: Any?) { + postNameObjectCallsCount += 1 + postNameObjectReceivedArguments = (aName: aName, anObject: anObject) + postNameObjectReceivedInvocations.append((aName: aName, anObject: anObject)) + postNameObjectClosure?(aName, anObject) + } +} class NotificationManagerMock: NotificationManagerProtocol { var delegate: NotificationManagerDelegate? @@ -1365,6 +1384,39 @@ class UserNotificationCenterMock: UserNotificationCenterProtocol { return requestAuthorizationOptionsReturnValue } } + //MARK: - deliveredNotifications + + var deliveredNotificationsCallsCount = 0 + var deliveredNotificationsCalled: Bool { + return deliveredNotificationsCallsCount > 0 + } + var deliveredNotificationsReturnValue: [UNNotification]! + var deliveredNotificationsClosure: (() async -> [UNNotification])? + + func deliveredNotifications() async -> [UNNotification] { + deliveredNotificationsCallsCount += 1 + if let deliveredNotificationsClosure = deliveredNotificationsClosure { + return await deliveredNotificationsClosure() + } else { + return deliveredNotificationsReturnValue + } + } + //MARK: - removeDeliveredNotifications + + var removeDeliveredNotificationsWithIdentifiersCallsCount = 0 + var removeDeliveredNotificationsWithIdentifiersCalled: Bool { + return removeDeliveredNotificationsWithIdentifiersCallsCount > 0 + } + var removeDeliveredNotificationsWithIdentifiersReceivedIdentifiers: [String]? + var removeDeliveredNotificationsWithIdentifiersReceivedInvocations: [[String]] = [] + var removeDeliveredNotificationsWithIdentifiersClosure: (([String]) -> Void)? + + func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { + removeDeliveredNotificationsWithIdentifiersCallsCount += 1 + removeDeliveredNotificationsWithIdentifiersReceivedIdentifiers = identifiers + removeDeliveredNotificationsWithIdentifiersReceivedInvocations.append(identifiers) + removeDeliveredNotificationsWithIdentifiersClosure?(identifiers) + } //MARK: - setNotificationCategories var setNotificationCategoriesCallsCount = 0 diff --git a/ElementX/Sources/Other/Extensions/NotificationName.swift b/ElementX/Sources/Other/Extensions/NotificationName.swift new file mode 100644 index 000000000..ace3bd116 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/NotificationName.swift @@ -0,0 +1,22 @@ +// +// Copyright 2023 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 Foundation + +extension Notification.Name { + static let roomMarkedAsRead = Notification.Name("roomMarkedAsRead") + static let invitesScreenAppeared = Notification.Name("invitesScreenAppeared") +} diff --git a/ElementX/Sources/Other/NotificationCenterProtocol.swift b/ElementX/Sources/Other/NotificationCenterProtocol.swift new file mode 100644 index 000000000..e99456513 --- /dev/null +++ b/ElementX/Sources/Other/NotificationCenterProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2023 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 Foundation + +// sourcery: AutoMockable +protocol NotificationCenterProtocol: AnyObject { + func post(name aName: NSNotification.Name, object anObject: Any?) +} + +extension NotificationCenter: NotificationCenterProtocol { } diff --git a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenModels.swift b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenModels.swift index a8b064ef7..c097d9dfe 100644 --- a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenModels.swift +++ b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenModels.swift @@ -48,4 +48,5 @@ struct InvitesScreenRoomDetails: Identifiable { enum InvitesScreenViewAction { case accept(InvitesScreenRoomDetails) case decline(InvitesScreenRoomDetails) + case appeared } diff --git a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenViewModel.swift b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenViewModel.swift index 6b34d7bf6..ca84b994c 100644 --- a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenViewModel.swift +++ b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenViewModel.swift @@ -24,6 +24,7 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel private let appSettings: AppSettings private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol + private let notificationCenterProtocol: NotificationCenterProtocol private let previouslySeenInvites: Set private let actionsSubject: PassthroughSubject = .init() @@ -36,11 +37,13 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel init(userSession: UserSessionProtocol, appSettings: AppSettings, analytics: AnalyticsService, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) { self.userSession = userSession self.appSettings = appSettings self.analytics = analytics self.userIndicatorController = userIndicatorController + self.notificationCenterProtocol = notificationCenterProtocol previouslySeenInvites = appSettings.seenInvites super.init(initialViewState: InvitesScreenViewState(), imageProvider: userSession.mediaProvider) @@ -55,6 +58,8 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel accept(invite: invite) case .decline(let invite): startDeclineFlow(invite: invite) + case .appeared: + notificationCenterProtocol.post(name: .invitesScreenAppeared, object: nil) } } diff --git a/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift b/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift index 1d45221de..56616674a 100644 --- a/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift +++ b/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift @@ -51,8 +51,11 @@ struct InvitesScreen: View { noInvitesContent } } + .onAppear { + context.send(viewAction: .appeared) + } } - + private var noInvitesContent: some View { Text(L10n.screenInvitesEmptyList) .font(.compound.bodyLG) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index c9ba1f618..0f1a45ac1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -33,18 +33,21 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let appSettings: AppSettings private let analytics: AnalyticsService private unowned let userIndicatorController: UserIndicatorControllerProtocol + private let notificationCenterProtocol: NotificationCenterProtocol init(timelineController: RoomTimelineControllerProtocol, mediaProvider: MediaProviderProtocol, roomProxy: RoomProxyProtocol, appSettings: AppSettings, analytics: AnalyticsService, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) { self.roomProxy = roomProxy self.timelineController = timelineController self.appSettings = appSettings self.analytics = analytics self.userIndicatorController = userIndicatorController + self.notificationCenterProtocol = notificationCenterProtocol super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID, roomTitle: roomProxy.roomTitle, @@ -214,6 +217,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } private func markRoomAsRead() async { + notificationCenterProtocol.post(name: .roomMarkedAsRead, object: roomProxy.id) _ = await timelineController.markRoomAsRead() } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 6a6d85bde..62c877dcd 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -19,7 +19,7 @@ import Foundation import UIKit import UserNotifications -class NotificationManager: NSObject, NotificationManagerProtocol { +final class NotificationManager: NSObject, NotificationManagerProtocol { private let notificationCenter: UserNotificationCenterProtocol private let appSettings: AppSettings @@ -31,6 +31,8 @@ class NotificationManager: NSObject, NotificationManagerProtocol { appSettings: AppSettings) { self.notificationCenter = notificationCenter self.appSettings = appSettings + super.init() + addObservers() } // MARK: NotificationManagerProtocol @@ -169,6 +171,47 @@ class NotificationManager: NSObject, NotificationManagerProtocol { delegate?.unregisterForRemoteNotifications() } } + + private func addObservers() { + NotificationCenter + .default + .publisher(for: .roomMarkedAsRead) + .sink { [weak self] notification in + guard let roomID = notification.object as? String else { + return + } + self?.removeMessageNotifications(for: roomID) + } + .store(in: &cancellables) + + NotificationCenter + .default + .publisher(for: .invitesScreenAppeared) + .sink { [weak self] _ in + self?.removeInviteNotifications() + } + .store(in: &cancellables) + } + + private func removeMessageNotifications(for roomID: String) { + Task { + let notificationsIdentifiers = await notificationCenter + .deliveredNotifications() + .filter { $0.request.content.roomID == roomID } + .map(\.request.identifier) + notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers) + } + } + + private func removeInviteNotifications() { + Task { + let notificationsIdentifiers = await notificationCenter + .deliveredNotifications() + .filter { $0.request.content.categoryIdentifier == NotificationConstants.Category.invite } + .map(\.request.identifier) + notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers) + } + } } // MARK: - UNUserNotificationCenterDelegate diff --git a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift index 40e46a610..4ecf7c2b5 100644 --- a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift @@ -17,10 +17,14 @@ import Foundation import UserNotifications +// Sadly we can't mock this because the delegate needs to be weak +// Use UserNotificationCenterSpy protocol UserNotificationCenterProtocol: AnyObject { var delegate: UNUserNotificationCenterDelegate? { get set } func add(_ request: UNNotificationRequest) async throws func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool + func deliveredNotifications() async -> [UNNotification] + func removeDeliveredNotifications(withIdentifiers identifiers: [String]) func setNotificationCategories(_ categories: Set) func authorizationStatus() async -> UNAuthorizationStatus } diff --git a/UnitTests/Sources/InvitesScreenViewModelTests.swift b/UnitTests/Sources/InvitesScreenViewModelTests.swift index eb3b129d9..41aaeff8e 100644 --- a/UnitTests/Sources/InvitesScreenViewModelTests.swift +++ b/UnitTests/Sources/InvitesScreenViewModelTests.swift @@ -22,6 +22,7 @@ class InvitesScreenViewModelTests: XCTestCase { var viewModel: InvitesScreenViewModelProtocol! var clientProxy: MockClientProxy! var userSession: MockUserSession! + var mockNotificationCenter: NotificationCenterMock! var context: InvitesScreenViewModelType.Context { viewModel.context @@ -30,6 +31,7 @@ class InvitesScreenViewModelTests: XCTestCase { override func setUpWithError() throws { clientProxy = MockClientProxy(userID: "@a:b.com") userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) + mockNotificationCenter = NotificationCenterMock() } func testEmptyState() async throws { @@ -72,6 +74,14 @@ class InvitesScreenViewModelTests: XCTestCase { context.send(viewAction: .decline(.init(roomDetails: details, isUnread: false))) XCTAssertNotNil(context.alertInfo) } + + func testHasAppeared() async { + setupViewModel() + context.send(viewAction: .appeared) + + await Task.yield() + XCTAssertEqual(mockNotificationCenter.postNameObjectReceivedArguments?.aName, .invitesScreenAppeared) + } // MARK: - Private @@ -85,6 +95,7 @@ class InvitesScreenViewModelTests: XCTestCase { viewModel = InvitesScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + notificationCenterProtocol: mockNotificationCenter) } } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index f9a320147..a9225071a 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -14,9 +14,10 @@ // limitations under the License. // +import Combine +import NotificationCenter import XCTest -import Combine @testable import ElementX final class NotificationManagerTests: XCTestCase { @@ -176,6 +177,32 @@ final class NotificationManagerTests: XCTestCase { await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response) XCTAssertTrue(notificationTappedDelegateCalled) } + + func test_MessageNotificationsRemoval() async throws { + // No interaction if the object is nil or of the wrong type + NotificationCenter.default.post(name: .roomMarkedAsRead, object: nil) + try await Task.sleep(for: .microseconds(200)) + XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 0) + XCTAssertEqual(notificationCenter.removeDeliveredNotificationsCallsCount, 0) + + NotificationCenter.default.post(name: .roomMarkedAsRead, object: 1) + try await Task.sleep(for: .microseconds(200)) + XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 0) + XCTAssertEqual(notificationCenter.removeDeliveredNotificationsCallsCount, 0) + + // The center calls the delivered and the removal functions when an id is passed + NotificationCenter.default.post(name: .roomMarkedAsRead, object: "RoomID") + try await Task.sleep(for: .microseconds(200)) + XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 1) + XCTAssertEqual(notificationCenter.removeDeliveredNotificationsCallsCount, 1) + } + + func test_InvitesNotificationsRemoval() async throws { + NotificationCenter.default.post(name: .invitesScreenAppeared, object: nil) + try await Task.sleep(for: .microseconds(200)) + XCTAssertEqual(notificationCenter.deliveredNotificationsCallsCount, 1) + XCTAssertEqual(notificationCenter.removeDeliveredNotificationsCallsCount, 1) + } } extension NotificationManagerTests: NotificationManagerDelegate { diff --git a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift index 84a62bfd6..598135ff7 100644 --- a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift +++ b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift @@ -17,9 +17,20 @@ import Foundation import UserNotifications -class UserNotificationCenterSpy: UserNotificationCenterProtocol { +final class UserNotificationCenterSpy: UserNotificationCenterProtocol { weak var delegate: UNUserNotificationCenterDelegate? - + + var deliveredNotificationsCallsCount = 0 + func deliveredNotifications() async -> [UNNotification] { + deliveredNotificationsCallsCount += 1 + return [] + } + + var removeDeliveredNotificationsCallsCount = 0 + func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { + removeDeliveredNotificationsCallsCount += 1 + } + var addRequest: UNNotificationRequest? func add(_ request: UNNotificationRequest) async throws { addRequest = request diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index f1d9924b1..5bda66676 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -349,6 +349,27 @@ class RoomScreenViewModelTests: XCTestCase { await Task.yield() XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0) } + + func testMarkAsRead() async { + // Setup + let notificationCenterMock = NotificationCenterMock() + let timelineController = MockRoomTimelineController() + let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + + let viewModel = RoomScreenViewModel(timelineController: timelineController, + mediaProvider: MockMediaProvider(), + roomProxy: roomProxyMock, + appSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics, + userIndicatorController: userIndicatorControllerMock, + notificationCenterProtocol: notificationCenterMock) + + viewModel.context.send(viewAction: .markRoomAsRead) + await Task.yield() + XCTAssertEqual(notificationCenterMock.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead) + let roomID = notificationCenterMock.postNameObjectReceivedArguments?.anObject as? String + XCTAssertEqual(roomID, roomProxyMock.id) + } } private extension TextRoomTimelineItem { diff --git a/changelog.d/1277.bugfix b/changelog.d/1277.bugfix new file mode 100644 index 000000000..f44f5df04 --- /dev/null +++ b/changelog.d/1277.bugfix @@ -0,0 +1 @@ +Push notifications for a room are cleared from the notification centre when opening its timeline. Same for invitations when opening the invite screen. \ No newline at end of file