diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ff05004b0..3ec0ad1a2 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -142,6 +142,15 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -160,15 +169,33 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "c6e2241daa46e5c6e5027a93b161bca6ba692bcc", + "version" : "0.12.0" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", "version" : "1.11.0" } }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-url-routing", + "state" : { + "revision" : "2f4f0404b3de0a0711feb7190f724d8a80bc1cfd", + "version" : "0.5.0" + } + }, { "identity" : "swiftstate", "kind" : "remoteSourceControl", @@ -204,6 +231,15 @@ "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", "version" : "2.0.1" } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", + "version" : "0.8.4" + } } ], "version" : 2 diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 53b5f6bb5..b93b2e530 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -47,7 +47,9 @@ class AppCoordinator: AppCoordinatorProtocol { private var userSessionCancellables = Set() private var cancellables = Set() - private(set) var notificationManager: NotificationManagerProtocol? + let notificationManager: NotificationManagerProtocol + + @Consumable private var storedAppRoute: AppRoute? init() { navigationRootCoordinator = NavigationRootCoordinator() @@ -66,6 +68,10 @@ class AppCoordinator: AppCoordinatorProtocol { } userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService) + + notificationManager = NotificationManager() + notificationManager.delegate = self + notificationManager.start() guard let currentVersion = Version(InfoPlistReader(bundle: .main).bundleShortVersionString) else { fatalError("The app's version number **must** use semver for migration purposes.") @@ -259,6 +265,10 @@ class AppCoordinator: AppCoordinatorProtocol { self.userSessionFlowCoordinator = userSessionFlowCoordinator navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator) + + if let storedAppRoute { + userSessionFlowCoordinator.handleAppRoute(storedAppRoute) + } } private func logout(isSoft: Bool) { @@ -292,11 +302,10 @@ class AppCoordinator: AppCoordinatorProtocol { userSession = nil userSessionFlowCoordinator = nil - - notificationManager?.delegate = nil - notificationManager = nil - } + notificationManager.setClientProxy(nil) + } + private func presentSplashScreen(isSoftLogout: Bool = false) { navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator()) @@ -308,34 +317,23 @@ class AppCoordinator: AppCoordinatorProtocol { } private func configureNotificationManager() { - guard ServiceLocator.shared.settings.enableNotifications else { - return - } - guard notificationManager == nil else { - return - } + notificationManager.setClientProxy(userSession.clientProxy) + notificationManager.requestAuthorization() - let manager = NotificationManager(clientProxy: userSession.clientProxy) - if manager.isAvailable { - manager.delegate = self - notificationManager = manager - manager.start() - - if let appDelegate = AppDelegate.shared { - appDelegate.callbacks - .receive(on: DispatchQueue.main) - .sink { [weak self] callback in - switch callback { - case .registeredNotifications(let deviceToken): - Task { await self?.notificationManager?.register(with: deviceToken) } - case .failedToRegisteredNotifications(let error): - self?.notificationManager?.registrationFailed(with: error) - } + if let appDelegate = AppDelegate.shared { + appDelegate.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + switch callback { + case .registeredNotifications(let deviceToken): + Task { await self?.notificationManager.register(with: deviceToken) } + case .failedToRegisteredNotifications(let error): + self?.notificationManager.registrationFailed(with: error) } - .store(in: &cancellables) - } else { - MXLog.error("Couldn't register to AppDelegate callbacks") - } + } + .store(in: &cancellables) + } else { + MXLog.error("Couldn't register to AppDelegate callbacks") } } @@ -437,6 +435,14 @@ class AppCoordinator: AppCoordinatorProtocol { } }.store(in: &cancellables) } + + private func handleAppRoute(_ appRoute: AppRoute) { + if let userSessionFlowCoordinator { + userSessionFlowCoordinator.handleAppRoute(appRoute) + } else { + storedAppRoute = appRoute + } + } } // MARK: - AuthenticationCoordinatorDelegate @@ -472,11 +478,14 @@ extension AppCoordinator: NotificationManagerDelegate { MXLog.info("[AppCoordinator] tappedNotification") // We store the room identifier into the thread identifier - guard !content.threadIdentifier.isEmpty else { + guard !content.threadIdentifier.isEmpty, + content.receiverID != nil else { return } - userSessionFlowCoordinator?.handleAppRoute(.room(roomID: content.threadIdentifier)) + // Handle here the account switching when available + + handleAppRoute(.room(roomID: content.threadIdentifier)) } func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async { diff --git a/ElementX/Sources/Application/AppCoordinatorProtocol.swift b/ElementX/Sources/Application/AppCoordinatorProtocol.swift index e76e4384b..4cff930e3 100644 --- a/ElementX/Sources/Application/AppCoordinatorProtocol.swift +++ b/ElementX/Sources/Application/AppCoordinatorProtocol.swift @@ -17,5 +17,5 @@ import Foundation protocol AppCoordinatorProtocol: CoordinatorProtocol { - var notificationManager: NotificationManagerProtocol? { get } + var notificationManager: NotificationManagerProtocol { get } } diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 0d50a3e42..c7b4d43e8 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -94,9 +94,7 @@ final class AppSettings: ObservableObject { } let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify") - - let enableNotifications = true - + // MARK: - Bug report let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports") diff --git a/ElementX/Sources/Application/Navigation/AppRouter.swift b/ElementX/Sources/Application/Navigation/AppRouter.swift index 56aa256c5..bdcf8cc82 100644 --- a/ElementX/Sources/Application/Navigation/AppRouter.swift +++ b/ElementX/Sources/Application/Navigation/AppRouter.swift @@ -16,6 +16,40 @@ import Foundation +import URLRouting + enum AppRoute { case room(roomID: String) } + +struct AppRouterManager { + private let deeplinkRouter = OneOf { + Route(.case(AppRoute.room(roomID:))) { + // Check with product if this is the expect path + Path { "room" } + Query { + Field("id") { Parse(.string) } + } + } + } + + private let permalinkRouter = OneOf { + Route(.case(AppRoute.room(roomID:))) { + Host("matrix.to") + Path { + "#" + Parse(.string) + } + } + } + + func route(from url: URL) -> AppRoute? { + var route: AppRoute? + if let deeplinkRoute = try? deeplinkRouter.match(url: url) { + route = deeplinkRoute + } else if let permalinkRoute = try? permalinkRouter.match(url: url) { + route = permalinkRoute + } + return route + } +} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 5158b5d46..2f1c08a56 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -50,6 +50,103 @@ class BugReportServiceMock: BugReportServiceProtocol { } } } +class NotificationManagerMock: NotificationManagerProtocol { + var delegate: NotificationManagerDelegate? + + //MARK: - start + + var startCallsCount = 0 + var startCalled: Bool { + return startCallsCount > 0 + } + var startClosure: (() -> Void)? + + func start() { + startCallsCount += 1 + startClosure?() + } + //MARK: - register + + var registerWithCallsCount = 0 + var registerWithCalled: Bool { + return registerWithCallsCount > 0 + } + var registerWithReceivedDeviceToken: Data? + var registerWithReceivedInvocations: [Data] = [] + var registerWithReturnValue: Bool! + var registerWithClosure: ((Data) async -> Bool)? + + func register(with deviceToken: Data) async -> Bool { + registerWithCallsCount += 1 + registerWithReceivedDeviceToken = deviceToken + registerWithReceivedInvocations.append(deviceToken) + if let registerWithClosure = registerWithClosure { + return await registerWithClosure(deviceToken) + } else { + return registerWithReturnValue + } + } + //MARK: - registrationFailed + + var registrationFailedWithCallsCount = 0 + var registrationFailedWithCalled: Bool { + return registrationFailedWithCallsCount > 0 + } + var registrationFailedWithReceivedError: Error? + var registrationFailedWithReceivedInvocations: [Error] = [] + var registrationFailedWithClosure: ((Error) -> Void)? + + func registrationFailed(with error: Error) { + registrationFailedWithCallsCount += 1 + registrationFailedWithReceivedError = error + registrationFailedWithReceivedInvocations.append(error) + registrationFailedWithClosure?(error) + } + //MARK: - showLocalNotification + + var showLocalNotificationWithSubtitleCallsCount = 0 + var showLocalNotificationWithSubtitleCalled: Bool { + return showLocalNotificationWithSubtitleCallsCount > 0 + } + var showLocalNotificationWithSubtitleReceivedArguments: (title: String, subtitle: String?)? + var showLocalNotificationWithSubtitleReceivedInvocations: [(title: String, subtitle: String?)] = [] + var showLocalNotificationWithSubtitleClosure: ((String, String?) async -> Void)? + + func showLocalNotification(with title: String, subtitle: String?) async { + showLocalNotificationWithSubtitleCallsCount += 1 + showLocalNotificationWithSubtitleReceivedArguments = (title: title, subtitle: subtitle) + showLocalNotificationWithSubtitleReceivedInvocations.append((title: title, subtitle: subtitle)) + await showLocalNotificationWithSubtitleClosure?(title, subtitle) + } + //MARK: - setClientProxy + + var setClientProxyCallsCount = 0 + var setClientProxyCalled: Bool { + return setClientProxyCallsCount > 0 + } + var setClientProxyReceivedClientProxy: ClientProxyProtocol? + var setClientProxyReceivedInvocations: [ClientProxyProtocol?] = [] + var setClientProxyClosure: ((ClientProxyProtocol?) -> Void)? + + func setClientProxy(_ clientProxy: ClientProxyProtocol?) { + setClientProxyCallsCount += 1 + setClientProxyReceivedClientProxy = clientProxy + setClientProxyReceivedInvocations.append(clientProxy) + setClientProxyClosure?(clientProxy) + } + //MARK: - requestAuthorization + + var requestAuthorizationCallsCount = 0 + var requestAuthorizationCalled: Bool { + return requestAuthorizationCallsCount > 0 + } + var requestAuthorizationClosure: (() -> Void)? + + func requestAuthorization() { + requestAuthorizationCallsCount += 1 + requestAuthorizationClosure?() + } +} class RoomMemberProxyMock: RoomMemberProxyProtocol { var userID: String { get { return underlyingUserID } diff --git a/ElementX/Sources/Other/Consumable.swift b/ElementX/Sources/Other/Consumable.swift new file mode 100644 index 000000000..15716bb74 --- /dev/null +++ b/ElementX/Sources/Other/Consumable.swift @@ -0,0 +1,37 @@ +// +// 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 + +@propertyWrapper struct Consumable { + var wrappedValue: Value? { + mutating get { + defer { + value = nil + } + return value + } + set { + value = newValue + } + } + + private var value: Value? + + init(value: Value? = nil) { + self.value = value + } +} diff --git a/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift similarity index 53% rename from ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift rename to ElementX/Sources/Other/Extensions/UNNotificationContent.swift index 7891e7665..00e372ab8 100644 --- a/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -1,5 +1,5 @@ // -// Copyright 2022 New Vector Ltd +// 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. @@ -15,23 +15,21 @@ // import Foundation +import UserNotifications -class MockNotificationManager: NotificationManagerProtocol { - // MARK: NotificationManagerProtocol - - var isAvailable: Bool { - false +extension UNNotificationContent { + @objc var receiverID: String? { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String + } +} + +extension UNMutableNotificationContent { + override var receiverID: String? { + get { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String + } + set { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] = newValue + } } - - weak var delegate: NotificationManagerDelegate? - - func start() { - delegate?.authorizationStatusUpdated(self, granted: false) - } - - func register(with deviceToken: Data) async -> Bool { false } - - func registrationFailed(with error: Error) { } - - func showLocalNotification(with title: String, subtitle: String?) { } } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 7ff45b8ec..dbaf5b6bc 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -20,10 +20,9 @@ import UserNotifications class NotificationManager: NSObject, NotificationManagerProtocol { private let notificationCenter: UserNotificationCenterProtocol - private let clientProxy: ClientProxyProtocol + private var clientProxy: ClientProxyProtocol? - init(clientProxy: ClientProxyProtocol, notificationCenter: UserNotificationCenterProtocol = UNUserNotificationCenter.current()) { - self.clientProxy = clientProxy + init(notificationCenter: UserNotificationCenterProtocol = UNUserNotificationCenter.current()) { self.notificationCenter = notificationCenter super.init() } @@ -32,10 +31,6 @@ class NotificationManager: NSObject, NotificationManagerProtocol { weak var delegate: NotificationManagerDelegate? - var isAvailable: Bool { - true - } - func start() { let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, title: L10n.actionQuickReply, @@ -46,6 +41,9 @@ class NotificationManager: NSObject, NotificationManagerProtocol { options: []) notificationCenter.setNotificationCategories([replyCategory]) notificationCenter.delegate = self + } + + func requestAuthorization() { Task { do { let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) @@ -60,7 +58,14 @@ class NotificationManager: NSObject, NotificationManagerProtocol { } func register(with deviceToken: Data) async -> Bool { - await setPusher(with: deviceToken, clientProxy: clientProxy) + guard let clientProxy else { + return false + } + return await setPusher(with: deviceToken, clientProxy: clientProxy) + } + + func setClientProxy(_ clientProxy: ClientProxyProtocol?) { + self.clientProxy = clientProxy } func registrationFailed(with error: Error) { } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift index e93353c2e..761df9821 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -31,12 +31,14 @@ protocol NotificationManagerDelegate: AnyObject { // MARK: - NotificationManagerProtocol -protocol NotificationManagerProtocol { - var isAvailable: Bool { get } +// sourcery: AutoMockable +protocol NotificationManagerProtocol: AnyObject { var delegate: NotificationManagerDelegate? { get set } func start() func register(with deviceToken: Data) async -> Bool func registrationFailed(with error: Error) func showLocalNotification(with title: String, subtitle: String?) async + func setClientProxy(_ clientProxy: ClientProxyProtocol?) + func requestAuthorization() } diff --git a/ElementX/Sources/Services/Notification/NotificationConstants.swift b/ElementX/Sources/Services/Notification/NotificationConstants.swift index e3670a203..882bced9b 100644 --- a/ElementX/Sources/Services/Notification/NotificationConstants.swift +++ b/ElementX/Sources/Services/Notification/NotificationConstants.swift @@ -22,6 +22,7 @@ enum NotificationConstants { static let eventIdentifier = "event_id" static let unreadCount = "unread_count" static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier" + static let receiverIdentifier = "receiver_id" } enum Category { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 7907b1d5b..4ee8c622a 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -20,7 +20,7 @@ import UIKit class UITestsAppCoordinator: AppCoordinatorProtocol { private let navigationRootCoordinator: NavigationRootCoordinator private var mockScreen: MockScreen? - var notificationManager: NotificationManagerProtocol? + let notificationManager: NotificationManagerProtocol = NotificationManagerMock() init() { UIView.setAnimationsEnabled(false) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 2c964b5d4..ab2fab0ff 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -146,6 +146,7 @@ targets: - package: SwiftState - package: GZIP - package: Sentry + - package: URLRouting - package: Version sources: diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 8300fc411..c5f14a81e 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -88,7 +88,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension { // First process without a media proxy. // After this some properties of the notification should be set, like title, subtitle, sound etc. - guard let firstContent = try await itemProxy.process(with: roomId, + guard let firstContent = try await itemProxy.process(receiverId: credentials.userID, + roomId: roomId, mediaProvider: nil) else { MXLog.error("\(tag) not even first content") @@ -109,7 +110,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension { MXLog.info("\(tag) process with media") // There is some media to load, process it again - if let latestContent = try await itemProxy.process(with: roomId, + if let latestContent = try await itemProxy.process(receiverId: credentials.userID, + roomId: roomId, mediaProvider: createMediaProvider(with: credentials)) { // Processing finished, hopefully with some media modifiedContent = latestContent diff --git a/NSE/Sources/Other/NotificationItemProxy+NSE.swift b/NSE/Sources/Other/NotificationItemProxy+NSE.swift index fb617b875..d0f658e09 100644 --- a/NSE/Sources/Other/NotificationItemProxy+NSE.swift +++ b/NSE/Sources/Other/NotificationItemProxy+NSE.swift @@ -48,10 +48,12 @@ extension NotificationItemProxy { /// Process the receiver item proxy /// - Parameters: + /// - receiverId: identifier of the user that has received the notification /// - roomId: Room identifier /// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations. /// - Returns: A notification content object if the notification should be displayed. Otherwise nil. - func process(with roomId: String, + func process(receiverId: String, + roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { // switch timelineItemProxy { // case .event(let eventItem): @@ -75,7 +77,7 @@ extension NotificationItemProxy { // For now we can't solve the sender ID nor get the type of message that we are displaying // so we are just going to process all of them as a text notification saying "Notification" let content = TextMessageContent(body: L10n.notification, formatted: nil) - return try await processText(content: content, senderId: "undefined", roomId: roomId, mediaProvider: mediaProvider) + return try await processText(content: content, receiverId: receiverId, senderId: "undefined", roomId: roomId, mediaProvider: mediaProvider) } // MARK: - Private @@ -83,42 +85,50 @@ extension NotificationItemProxy { // MARK: Common private func process(message: Message, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { switch message.msgtype() { case .text(content: let content): return try await processText(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .image(content: let content): return try await processImage(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .audio(content: let content): return try await processAudio(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .video(content: let content): return try await processVideo(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .file(content: let content): return try await processFile(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .notice(content: let content): return try await processNotice(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) case .emote(content: let content): return try await processEmote(content: content, + receiverId: receiverId, senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) @@ -127,10 +137,12 @@ extension NotificationItemProxy { } } - private func processCommon(senderId: String, + private func processCommon(receiverId: String, + senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { var notification = UNMutableNotificationContent() + notification.receiverID = receiverId notification.title = title if let subtitle = subtitle { notification.subtitle = subtitle @@ -153,10 +165,12 @@ extension NotificationItemProxy { // MARK: Message Types private func processText(content: TextMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - let notification = try await processCommon(senderId: senderId, + let notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = content.body @@ -165,10 +179,12 @@ extension NotificationItemProxy { } private func processImage(content: ImageMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - var notification = try await processCommon(senderId: senderId, + var notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "📷 " + content.body @@ -180,10 +196,12 @@ extension NotificationItemProxy { } private func processVideo(content: VideoMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - var notification = try await processCommon(senderId: senderId, + var notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "📹 " + content.body @@ -195,10 +213,12 @@ extension NotificationItemProxy { } private func processFile(content: FileMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - let notification = try await processCommon(senderId: senderId, + let notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "📄 " + content.body @@ -207,10 +227,12 @@ extension NotificationItemProxy { } private func processNotice(content: NoticeMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - let notification = try await processCommon(senderId: senderId, + let notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "❕ " + content.body @@ -219,10 +241,12 @@ extension NotificationItemProxy { } private func processEmote(content: EmoteMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - let notification = try await processCommon(senderId: senderId, + let notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "🫥 " + content.body @@ -231,10 +255,12 @@ extension NotificationItemProxy { } private func processAudio(content: AudioMessageContent, + receiverId: String, senderId: String, roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - let notification = try await processCommon(senderId: senderId, + let notification = try await processCommon(receiverId: receiverId, + senderId: senderId, roomId: roomId, mediaProvider: mediaProvider) notification.body = "🔊 " + content.body diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index e6878da88..f5195730a 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -89,3 +89,4 @@ targets: - path: ../../ElementX/Sources/Other/Extensions/UTType.swift - path: ../../ElementX/Sources/Other/AvatarSize.swift - path: ../../ElementX/Sources/Other/InfoPlistReader.swift + - path: ../../ElementX/Sources/Other/Extensions/UNNotificationContent.swift diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index c8772e899..82cfab981 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -30,7 +30,9 @@ final class NotificationManagerTests: XCTestCase { private let settings = ServiceLocator.shared.settings override func setUp() { - notificationManager = NotificationManager(clientProxy: clientProxy, notificationCenter: notificationCenter) + notificationManager = NotificationManager(notificationCenter: notificationCenter) + notificationManager.start() + notificationManager.setClientProxy(clientProxy) } func test_whenRegistered_pusherIsCalled() async { @@ -95,7 +97,6 @@ final class NotificationManagerTests: XCTestCase { } func test_whenStart_notificationCategoriesAreSet() throws { - notificationManager.start() let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, title: L10n.actionQuickReply, options: []) @@ -107,13 +108,12 @@ final class NotificationManagerTests: XCTestCase { } func test_whenStart_delegateIsSet() throws { - notificationManager.start() let delegate = try XCTUnwrap(notificationCenter.delegate) XCTAssertTrue(delegate.isEqual(notificationManager)) } func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws { - notificationManager.start() + notificationManager.requestAuthorization() await Task.yield() XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge]) } @@ -122,13 +122,13 @@ final class NotificationManagerTests: XCTestCase { authorizationStatusWasGranted = false notificationCenter.requestAuthorizationGrantedReturnValue = true notificationManager.delegate = self - notificationManager.start() + + notificationManager.requestAuthorization() try await Task.sleep(for: .milliseconds(100)) XCTAssertTrue(authorizationStatusWasGranted) } func test_whenWillPresentNotificationsDelegateNotSet_CorrectPresentationOptionsReturned() async throws { - notificationManager.start() let archiver = MockCoder(requiringSecureCoding: false) let notification = try XCTUnwrap(UNNotification(coder: archiver)) let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) @@ -138,7 +138,7 @@ final class NotificationManagerTests: XCTestCase { func test_whenWillPresentNotificationsDelegateSetAndNotificationsShoudNotBeDisplayed_CorrectPresentationOptionsReturned() async throws { shouldDisplayInAppNotificationReturnValue = false notificationManager.delegate = self - notificationManager.start() + let notification = try UNNotification.with(userInfo: [AnyHashable: Any]()) let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) XCTAssertEqual(options, []) @@ -147,7 +147,7 @@ final class NotificationManagerTests: XCTestCase { func test_whenWillPresentNotificationsDelegateSetAndNotificationsShoudBeDisplayed_CorrectPresentationOptionsReturned() async throws { shouldDisplayInAppNotificationReturnValue = true notificationManager.delegate = self - notificationManager.start() + let notification = try UNNotification.with(userInfo: [AnyHashable: Any]()) let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) XCTAssertEqual(options, [.badge, .sound, .list, .banner]) @@ -156,7 +156,6 @@ final class NotificationManagerTests: XCTestCase { func test_whenNotificationCenterReceivedResponseInLineReply_delegateIsCalled() async throws { handleInlineReplyDelegateCalled = false notificationManager.delegate = self - notificationManager.start() let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: NotificationConstants.Action.inlineReply) await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response) XCTAssertTrue(handleInlineReplyDelegateCalled) @@ -165,7 +164,6 @@ final class NotificationManagerTests: XCTestCase { func test_whenNotificationCenterReceivedResponseWithActionIdentifier_delegateIsCalled() async throws { notificationTappedDelegateCalled = false notificationManager.delegate = self - notificationManager.start() let response = try UNTextInputNotificationResponse.with(userInfo: [AnyHashable: Any](), actionIdentifier: UNNotificationDefaultActionIdentifier) await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response) XCTAssertTrue(notificationTappedDelegateCalled) diff --git a/changelog.d/802.bugfix b/changelog.d/802.bugfix new file mode 100644 index 000000000..c1c2a3e57 --- /dev/null +++ b/changelog.d/802.bugfix @@ -0,0 +1 @@ +Notifications are now handled when the app is in a killed state. \ No newline at end of file diff --git a/project.yml b/project.yml index 5c26500ac..6653ca4dd 100644 --- a/project.yml +++ b/project.yml @@ -98,6 +98,9 @@ packages: SnapshotTesting: url: https://github.com/pointfreeco/swift-snapshot-testing minorVersion: 1.11.0 + URLRouting: + url: https://github.com/pointfreeco/swift-url-routing + minorVersion: 0.5.0 Version: url: https://github.com/mxcl/Version minorVersion: 2.0.0