diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index f5953b4bc..5339fa9c3 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -105,7 +105,8 @@ class ClientProxy: ClientProxyProtocol { let delegate = WeakClientProxyWrapper(clientProxy: self) client.setDelegate(delegate: delegate) - // Uncomment to test local notifications + + // Set up sync listener for generating local notifications. await Task.dispatch(on: clientQueue) { client.setNotificationDelegate(notificationDelegate: delegate) } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 7d2a70aec..1fdd6b175 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -104,11 +104,13 @@ class NotificationManager: NSObject, NotificationManagerProtocol { guard let userSession, notification.event.timestamp > ServiceLocator.shared.settings.lastLaunchDate else { return } do { - guard let content = try await notification.process(mediaProvider: userSession.mediaProvider), - let identifier = notification.id else { + guard let identifier = notification.id else { return } + + let content = try await notification.process(mediaProvider: userSession.mediaProvider) let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil) + guard !ServiceLocator.shared.settings.servedNotificationIdentifiers.contains(identifier) else { MXLog.info("NotificationManager] local notification discarded because it has already been served") return diff --git a/ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift b/ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift deleted file mode 100644 index 6dbdf86d9..000000000 --- a/ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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 Foundation - -class MockNotificationServiceProxy: NotificationServiceProxyProtocol { - func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxyProtocol? { - nil - } -} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index 7b23b7ef0..c081b3faf 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -102,10 +102,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { } } -// The mock and the protocol are just temporary until we can handle -// and decrypt notifications both in background and in foreground -// but they should not be necessary in the future -struct MockNotificationItemProxy: NotificationItemProxyProtocol { +struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { let eventID: String var event: TimelineEventProxyProtocol { @@ -167,13 +164,13 @@ extension NotificationItemProxyProtocol { /// - 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(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { - if self is MockNotificationItemProxy { - return processMock() + func process(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + if self is EmptyNotificationItemProxy { + return processEmpty() } else { switch event.type { case .none, .state: - return nil + return processEmpty() case let .messageLike(content): switch content { case .roomMessage(messageType: let messageType): @@ -194,7 +191,7 @@ extension NotificationItemProxyProtocol { return try await processText(content: content, mediaProvider: mediaProvider) } default: - return nil + return processEmpty() } } } @@ -204,8 +201,7 @@ extension NotificationItemProxyProtocol { // MARK: - Private - // To be removed once we don't need the mock anymore - private func processMock() -> UNMutableNotificationContent { + private func processEmpty() -> UNMutableNotificationContent { let notification = UNMutableNotificationContent() notification.receiverID = receiverID notification.roomID = roomID diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift deleted file mode 100644 index a9bf07f1a..000000000 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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 Foundation -import MatrixRustSDK - -class NotificationServiceProxy: NotificationServiceProxyProtocol { - private let userID: String -// private let service: NotificationServiceProtocol - - init(basePath: String, - userID: String) { - self.userID = userID -// service = NotificationService(basePath: basePath, userId: userId) - } - - func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxyProtocol? { - MockNotificationItemProxy(eventID: eventId, roomID: roomId, receiverID: userID) - } -} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift deleted file mode 100644 index 043b614b1..000000000 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// 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 Foundation - -protocol NotificationServiceProxyProtocol { - func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxyProtocol? -} diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index d0d66b0b0..ba4236bdb 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -22,8 +22,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension { private let settings = NSESettings() private lazy var keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) - var handler: ((UNNotificationContent) -> Void)? - var modifiedContent: UNMutableNotificationContent? + private var handler: ((UNNotificationContent) -> Void)? + private var modifiedContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { @@ -51,9 +51,9 @@ class NotificationServiceExtension: UNNotificationServiceExtension { MXLog.info("\(tag) Payload came: \(request.content.userInfo)") Task { - try await run(with: credentials, - roomId: roomId, - eventId: eventId) + await run(with: credentials, + roomId: roomId, + eventId: eventId) } } @@ -66,80 +66,51 @@ class NotificationServiceExtension: UNNotificationServiceExtension { private func run(with credentials: KeychainCredentials, roomId: String, - eventId: String) async throws { + eventId: String) async { MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)") - let service = NotificationServiceProxy(basePath: URL.sessionsBaseDirectory.path, - userID: credentials.userID) + do { + let userSession = try NSEUserSession(credentials: credentials) + + let itemProxy = try await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) - guard let itemProxy = try await service.notificationItem(roomId: roomId, - eventId: eventId) else { - MXLog.error("\(tag) got no notification item") + // After the first processing, update the modified content + modifiedContent = try await itemProxy.process(mediaProvider: nil) - // Notification should be discarded - return discard() - } + guard itemProxy.requiresMediaProvider else { + MXLog.info("\(tag) no media needed") - // 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(mediaProvider: nil) else { - MXLog.error("\(tag) not even first content") + // We've processed the item and no media operations needed, so no need to go further + return notify() + } - // Notification should be discarded - return discard() - } + MXLog.info("\(tag) process with media") - // After the first processing, update the modified content - modifiedContent = firstContent - - guard itemProxy.requiresMediaProvider else { - MXLog.info("\(tag) no media needed") - - // We've processed the item and no media operations needed, so no need to go further + // There is some media to load, process it again + if let latestContent = try? await itemProxy.process(mediaProvider: userSession.mediaProvider) { + // Processing finished, hopefully with some media + modifiedContent = latestContent + } + // We still notify, but without the media attachment if it fails to load return notify() - } - - MXLog.info("\(tag) process with media") - - // There is some media to load, process it again - if let latestContent = try await itemProxy.process(mediaProvider: createMediaProvider(with: credentials)) { - // Processing finished, hopefully with some media - modifiedContent = latestContent - return notify() - } else { - // This is not very likely, as it should discard the notification sooner + } catch { + MXLog.error("NSE run error: \(error)") return discard() } } - - private func createMediaProvider(with credentials: KeychainCredentials) throws -> MediaProviderProtocol { - let builder = ClientBuilder() - .basePath(path: URL.sessionsBaseDirectory.path) - .username(username: credentials.userID) - - let client = try builder.build() - try client.restoreSession(session: credentials.restorationToken.session) - - MXLog.info("\(tag) creating media provider") - - return MediaProvider(mediaLoader: MediaLoader(client: client), - imageCache: .onlyOnDisk, - backgroundTaskService: nil) - } private func notify() { MXLog.info("\(tag) notify") guard let modifiedContent else { MXLog.info("\(tag) notify: no modified content") - return + return discard() } guard let identifier = modifiedContent.notificationID, !settings.servedNotificationIdentifiers.contains(identifier) else { MXLog.info("\(tag) notify: notification already served") - discard() - return + return discard() } settings.servedNotificationIdentifiers.insert(identifier) diff --git a/NSE/Sources/Other/NSEUserSession.swift b/NSE/Sources/Other/NSEUserSession.swift new file mode 100644 index 000000000..e98422941 --- /dev/null +++ b/NSE/Sources/Other/NSEUserSession.swift @@ -0,0 +1,47 @@ +// +// 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 +import MatrixRustSDK + +final class NSEUserSession { + private let client: ClientProtocol + private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: client), + imageCache: .onlyOnDisk, + backgroundTaskService: nil) + + init(credentials: KeychainCredentials) throws { + let builder = ClientBuilder() + .basePath(path: URL.sessionsBaseDirectory.path) + .username(username: credentials.userID) + + client = try builder.build() + try client.restoreSession(session: credentials.restorationToken.session) + } + + func notificationItemProxy(roomID: String, eventID: String) async throws -> NotificationItemProxyProtocol { + let userID = try client.userId() + return await Task.dispatch(on: .global()) { + do { + let notification = try self.client.getNotificationItem(roomId: roomID, eventId: eventID) + return NotificationItemProxy(notificationItem: notification, receiverID: userID) + } catch { + MXLog.error("NSE: Could not get notification's content creating an empty notification instead, error: \(error)") + return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: userID) + } + } + } +} diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 763c5710f..cc5adcd84 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -72,8 +72,6 @@ targets: - path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift - path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift - - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift - - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift - path: ../../ElementX/Sources/Services/Notification/NotificationConstants.swift - path: ../../ElementX/Sources/Services/Media/Provider diff --git a/changelog.d/855.feature b/changelog.d/855.feature new file mode 100644 index 000000000..3ea51a59f --- /dev/null +++ b/changelog.d/855.feature @@ -0,0 +1 @@ +Remote Push Notifications can now be displayed as rich push notifications. \ No newline at end of file