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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ extension Dictionary {
|
||||
options: [.fragmentsAllowed, .sortedKeys]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
33
ElementX/Sources/Other/Extensions/Encodable.swift
Normal file
33
ElementX/Sources/Other/Extensions/Encodable.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1
changelog.d/759.feature
Normal file
@@ -0,0 +1 @@
|
||||
Enabled Push Notifications with static text.
|
||||
Reference in New Issue
Block a user