From 88a498e538150af1a790e12e08f02e250a02a3ef Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:16:30 +0200 Subject: [PATCH] Notifications Enabled (#760) * Notifications Enabled with default static text notifications enabled with default static text code improvement added the share key * added the changelog * notification ID is added to the json only if it exists * renamed Bundle.mainApp to app and updated the strings from localazy * made a struct for the APNSPayload * APNS Payload fix --- .../Sources/Application/AppSettings.swift | 2 +- .../Generated/Strings+Untranslated.swift | 1 + .../Sources/Other/Extensions/Bundle.swift | 14 +++++- .../Sources/Other/Extensions/Dictionary.swift | 1 - .../Sources/Other/Extensions/Encodable.swift | 33 +++++++++++++ .../RoomDetails/View/RoomDetailsScreen.swift | 4 +- .../View/RoomMemberDetailsScreen.swift | 2 +- .../Notification/Manager/APNSPayload.swift | 47 +++++++++++++++++++ .../Manager/NotificationManager.swift | 32 ++++++------- .../Notification/NotificationConstants.swift | 1 + .../Proxy/NotificationItemProxy.swift | 6 +-- .../Proxy/NotificationServiceProxy.swift | 8 +--- .../UserSession/RestorationToken.swift | 13 +++++ .../NotificationServiceExtension.swift | 4 +- .../Other/NotificationItemProxy+NSE.swift | 4 +- NSE/Sources/Other/UNNotificationRequest.swift | 4 ++ .../NotificationManagerTests.swift | 15 ++---- changelog.d/759.feature | 1 + 18 files changed, 146 insertions(+), 46 deletions(-) create mode 100644 ElementX/Sources/Other/Extensions/Encodable.swift create mode 100644 ElementX/Sources/Services/Notification/Manager/APNSPayload.swift create mode 100644 changelog.d/759.feature diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 6ec02f2ce..e224a8bba 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -93,7 +93,7 @@ final class AppSettings: ObservableObject { let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify") - let enableNotifications = false + let enableNotifications = true // MARK: - Bug report diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 7075215f5..f13d64e79 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -71,6 +71,7 @@ public enum UntranslatedL10n { extension UntranslatedL10n { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // No need to check languages, we always default to en for untranslated strings guard let bundle = Bundle(for: BundleToken.self).lprojBundle(for: "en") else { // no translations for the desired language return key diff --git a/ElementX/Sources/Other/Extensions/Bundle.swift b/ElementX/Sources/Other/Extensions/Bundle.swift index 2a242a8b4..c9b838396 100644 --- a/ElementX/Sources/Other/Extensions/Bundle.swift +++ b/ElementX/Sources/Other/Extensions/Bundle.swift @@ -27,7 +27,7 @@ public extension Bundle { return bundle } - guard let lprojURL = url(forResource: language, withExtension: "lproj") else { + guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else { return nil } @@ -54,6 +54,18 @@ public extension Bundle { } } + static var app: Bundle { + var bundle = Bundle.main + if bundle.bundleURL.pathExtension == "appex" { + // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex + let url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() + if let otherBundle = Bundle(url: url) { + bundle = otherBundle + } + } + return bundle + } + /// Preferred languages in the priority order. private(set) static var preferredLanguages: [String] = calculatePreferredLanguages() diff --git a/ElementX/Sources/Other/Extensions/Dictionary.swift b/ElementX/Sources/Other/Extensions/Dictionary.swift index 3a9573d66..7e2c7e0a1 100644 --- a/ElementX/Sources/Other/Extensions/Dictionary.swift +++ b/ElementX/Sources/Other/Extensions/Dictionary.swift @@ -22,7 +22,6 @@ extension Dictionary { options: [.fragmentsAllowed, .sortedKeys]) else { return nil } - return String(data: data, encoding: .utf8) } } diff --git a/ElementX/Sources/Other/Extensions/Encodable.swift b/ElementX/Sources/Other/Extensions/Encodable.swift new file mode 100644 index 000000000..7c0c4e240 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/Encodable.swift @@ -0,0 +1,33 @@ +// +// 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 Encodable { + func toJsonDictionary(_ encoder: JSONEncoder = JSONEncoder()) throws -> [String: Any] { + let data = try encoder.encode(self) + let object = try JSONSerialization.jsonObject(with: data) + guard let json = object as? [String: Any] else { + let context = DecodingError.Context(codingPath: [], debugDescription: "Deserialized object is not a dictionary") + throw DecodingError.typeMismatch(type(of: object), context) + } + return json + } + + func toJsonString(_ encoder: JSONEncoder = JSONEncoder()) throws -> String? { + try toJsonDictionary(encoder).jsonString + } +} diff --git a/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift index 9c5469889..c754870d8 100644 --- a/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift @@ -69,7 +69,7 @@ struct RoomDetailsScreen: View { ShareLink(item: permalink) { Image(systemName: "square.and.arrow.up") } - .buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink)) + .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) } .padding(.top, 32) } @@ -90,7 +90,7 @@ struct RoomDetailsScreen: View { ShareLink(item: permalink) { Image(systemName: "square.and.arrow.up") } - .buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink)) + .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) } .padding(.top, 32) } diff --git a/ElementX/Sources/Screens/RoomMemberDetails/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetails/View/RoomMemberDetailsScreen.swift index 661813142..1bd63feea 100644 --- a/ElementX/Sources/Screens/RoomMemberDetails/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetails/View/RoomMemberDetailsScreen.swift @@ -48,7 +48,7 @@ struct RoomMemberDetailsScreen: View { ShareLink(item: permalink) { Image(systemName: "square.and.arrow.up") } - .buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink)) + .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) } .padding(.top, 32) } diff --git a/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift b/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift new file mode 100644 index 000000000..9ff966602 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Manager/APNSPayload.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 + +struct APSAlert: Encodable { + let locKey: String + let locArgs: [String] + + enum CodingKeys: String, CodingKey { + case locKey = "loc-key" + case locArgs = "loc-args" + } +} + +struct APSInfo: Encodable { + let mutableContent: Int + let alert: APSAlert + + enum CodingKeys: String, CodingKey { + case mutableContent = "mutable-content" + case alert + } +} + +struct APNSPayload: Encodable { + let aps: APSInfo + let pusherNotificationClientIdentifier: String? + + enum CodingKeys: String, CodingKey { + case aps + case pusherNotificationClientIdentifier = "pusher_notification_client_identifier" + } +} diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 795a57913..7ff45b8ec 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -84,24 +84,20 @@ class NotificationManager: NSObject, NotificationManagerProtocol { private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool { do { - let defaultPayload = [ - "aps": [ - "mutable-content": 1, - "alert": [ - "loc-key": "Notification", - "loc-args": [] - ] - ] - ] - let configuration = await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(), - appId: ServiceLocator.shared.settings.pusherAppId), - kind: .http(data: .init(url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString, - format: .eventIdOnly, - defaultPayload: defaultPayload.jsonString)), - appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)", - deviceDisplayName: UIDevice.current.name, - profileTag: pusherProfileTag(), - lang: Bundle.preferredLanguages.first ?? "en") + let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1, + alert: APSAlert(locKey: "Notification", + locArgs: [])), + pusherNotificationClientIdentifier: clientProxy.restorationToken?.pusherNotificationClientIdentifier) + + let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(), + appId: ServiceLocator.shared.settings.pusherAppId), + kind: .http(data: .init(url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString, + format: .eventIdOnly, + defaultPayload: defaultPayload.toJsonString())), + appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)", + deviceDisplayName: UIDevice.current.name, + profileTag: pusherProfileTag(), + lang: Bundle.preferredLanguages.first ?? "en") try await clientProxy.setPusher(with: configuration) MXLog.info("[NotificationManager] set pusher succeeded") return true diff --git a/ElementX/Sources/Services/Notification/NotificationConstants.swift b/ElementX/Sources/Services/Notification/NotificationConstants.swift index 018cdc5f5..e3670a203 100644 --- a/ElementX/Sources/Services/Notification/NotificationConstants.swift +++ b/ElementX/Sources/Services/Notification/NotificationConstants.swift @@ -21,6 +21,7 @@ enum NotificationConstants { static let roomIdentifier = "room_id" static let eventIdentifier = "event_id" static let unreadCount = "unread_count" + static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier" } enum Category { diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index 03f76f01b..cdf95cecb 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -52,15 +52,15 @@ struct NotificationItemProxy { // } var title: String { - "Title" + InfoPlistReader(bundle: .app).bundleDisplayName } var subtitle: String? { - nil + L10n.notification } var isNoisy: Bool { - true + false } var avatarURL: URL? { diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift index 619acaa29..f5903130c 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift @@ -26,12 +26,6 @@ class NotificationServiceProxy: NotificationServiceProxyProtocol { } func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? { - nil -// try await Task.dispatch(on: .global()) { -// guard let item = try self.service.getNotificationItem(roomId: roomId, eventId: eventId) else { -// return nil -// } -// return .init(notificationItem: item) -// } + NotificationItemProxy() } } diff --git a/ElementX/Sources/Services/UserSession/RestorationToken.swift b/ElementX/Sources/Services/UserSession/RestorationToken.swift index 287b2b0c4..c8fddc62b 100644 --- a/ElementX/Sources/Services/UserSession/RestorationToken.swift +++ b/ElementX/Sources/Services/UserSession/RestorationToken.swift @@ -14,11 +14,24 @@ // limitations under the License. // +import CryptoKit import Foundation + import MatrixRustSDK struct RestorationToken: Codable, Equatable { let session: MatrixRustSDK.Session + let pusherNotificationClientIdentifier: String? + + init(session: MatrixRustSDK.Session) { + self.session = session + if let data = session.userId.data(using: .utf8) { + let digest = SHA256.hash(data: data) + pusherNotificationClientIdentifier = digest.compactMap { String(format: "%02x", $0) }.joined() + } else { + pusherNotificationClientIdentifier = nil + } + } } extension MatrixRustSDK.Session: Codable { diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 510f05708..8300fc411 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -36,11 +36,13 @@ class NotificationServiceExtension: UNNotificationServiceExtension { guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), let roomId = request.roomId, let eventId = request.eventId, - let credentials = keychainController.restorationTokens().first else { + let notificationID = request.pusherNotificationClientIdentifier, + let credentials = keychainController.restorationTokens().first(where: { $0.restorationToken.pusherNotificationClientIdentifier == notificationID }) else { // We cannot process this notification, it might be due to one of these: // - Device rebooted and locked // - Not a Matrix notification // - User is not signed in + // - NotificationID could not be resolved return contentHandler(request.content) } diff --git a/NSE/Sources/Other/NotificationItemProxy+NSE.swift b/NSE/Sources/Other/NotificationItemProxy+NSE.swift index 9b3438f22..55b6b025f 100644 --- a/NSE/Sources/Other/NotificationItemProxy+NSE.swift +++ b/NSE/Sources/Other/NotificationItemProxy+NSE.swift @@ -53,7 +53,6 @@ extension NotificationItemProxy { /// - Returns: A notification content object if the notification should be displayed. Otherwise nil. func process(with roomId: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { - nil // switch timelineItemProxy { // case .event(let eventItem): // guard eventItem.isMessage else { @@ -73,6 +72,9 @@ extension NotificationItemProxy { // case .other: // return nil // } + // 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 common + try await processCommon(senderId: "undefined", roomId: roomId, mediaProvider: mediaProvider) } // MARK: - Private diff --git a/NSE/Sources/Other/UNNotificationRequest.swift b/NSE/Sources/Other/UNNotificationRequest.swift index 102437230..24c7116bb 100644 --- a/NSE/Sources/Other/UNNotificationRequest.swift +++ b/NSE/Sources/Other/UNNotificationRequest.swift @@ -29,4 +29,8 @@ extension UNNotificationRequest { var unreadCount: Int? { content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int } + + var pusherNotificationClientIdentifier: String? { + content.userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String + } } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 99a61fcf1..c8772e899 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -68,16 +68,11 @@ final class NotificationManagerTests: XCTestCase { } XCTAssertEqual(data.url, settings?.pushGatewayBaseURL.absoluteString) XCTAssertEqual(data.format, .eventIdOnly) - let defaultPayload: [AnyHashable: Any] = [ - "aps": [ - "mutable-content": 1, - "alert": [ - "loc-key": "Notification", - "loc-args": [] - ] - ] - ] - XCTAssertEqual(data.defaultPayload, defaultPayload.jsonString) + let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1, + alert: APSAlert(locKey: "Notification", + locArgs: [])), + pusherNotificationClientIdentifier: nil) + XCTAssertEqual(data.defaultPayload, try defaultPayload.toJsonString()) } func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async throws { diff --git a/changelog.d/759.feature b/changelog.d/759.feature new file mode 100644 index 000000000..19de725aa --- /dev/null +++ b/changelog.d/759.feature @@ -0,0 +1 @@ +Enabled Push Notifications with static text. \ No newline at end of file