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
This commit is contained in:
Mauro
2023-04-05 20:16:30 +02:00
committed by GitHub
parent b03bc997fc
commit 88a498e538
18 changed files with 146 additions and 46 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -22,7 +22,6 @@ extension Dictionary {
options: [.fragmentsAllowed, .sortedKeys]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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? {

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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 {

1
changelog.d/759.feature Normal file
View File

@@ -0,0 +1 @@
Enabled Push Notifications with static text.