mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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 pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify")
|
||||||
|
|
||||||
let enableNotifications = false
|
let enableNotifications = true
|
||||||
|
|
||||||
// MARK: - Bug report
|
// MARK: - Bug report
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public enum UntranslatedL10n {
|
|||||||
|
|
||||||
extension UntranslatedL10n {
|
extension UntranslatedL10n {
|
||||||
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
|
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 {
|
guard let bundle = Bundle(for: BundleToken.self).lprojBundle(for: "en") else {
|
||||||
// no translations for the desired language
|
// no translations for the desired language
|
||||||
return key
|
return key
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public extension Bundle {
|
|||||||
return 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
|
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.
|
/// Preferred languages in the priority order.
|
||||||
private(set) static var preferredLanguages: [String] = calculatePreferredLanguages()
|
private(set) static var preferredLanguages: [String] = calculatePreferredLanguages()
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ extension Dictionary {
|
|||||||
options: [.fragmentsAllowed, .sortedKeys]) else {
|
options: [.fragmentsAllowed, .sortedKeys]) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(data: data, encoding: .utf8)
|
return String(data: data, encoding: .utf8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
ShareLink(item: permalink) {
|
||||||
Image(systemName: "square.and.arrow.up")
|
Image(systemName: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink))
|
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||||
}
|
}
|
||||||
.padding(.top, 32)
|
.padding(.top, 32)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ struct RoomDetailsScreen: View {
|
|||||||
ShareLink(item: permalink) {
|
ShareLink(item: permalink) {
|
||||||
Image(systemName: "square.and.arrow.up")
|
Image(systemName: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink))
|
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||||
}
|
}
|
||||||
.padding(.top, 32)
|
.padding(.top, 32)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct RoomMemberDetailsScreen: View {
|
|||||||
ShareLink(item: permalink) {
|
ShareLink(item: permalink) {
|
||||||
Image(systemName: "square.and.arrow.up")
|
Image(systemName: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink))
|
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||||
}
|
}
|
||||||
.padding(.top, 32)
|
.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 {
|
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
||||||
do {
|
do {
|
||||||
let defaultPayload = [
|
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
|
||||||
"aps": [
|
alert: APSAlert(locKey: "Notification",
|
||||||
"mutable-content": 1,
|
locArgs: [])),
|
||||||
"alert": [
|
pusherNotificationClientIdentifier: clientProxy.restorationToken?.pusherNotificationClientIdentifier)
|
||||||
"loc-key": "Notification",
|
|
||||||
"loc-args": []
|
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,
|
||||||
let configuration = await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
|
defaultPayload: defaultPayload.toJsonString())),
|
||||||
appId: ServiceLocator.shared.settings.pusherAppId),
|
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
|
||||||
kind: .http(data: .init(url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString,
|
deviceDisplayName: UIDevice.current.name,
|
||||||
format: .eventIdOnly,
|
profileTag: pusherProfileTag(),
|
||||||
defaultPayload: defaultPayload.jsonString)),
|
lang: Bundle.preferredLanguages.first ?? "en")
|
||||||
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
|
|
||||||
deviceDisplayName: UIDevice.current.name,
|
|
||||||
profileTag: pusherProfileTag(),
|
|
||||||
lang: Bundle.preferredLanguages.first ?? "en")
|
|
||||||
try await clientProxy.setPusher(with: configuration)
|
try await clientProxy.setPusher(with: configuration)
|
||||||
MXLog.info("[NotificationManager] set pusher succeeded")
|
MXLog.info("[NotificationManager] set pusher succeeded")
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum NotificationConstants {
|
|||||||
static let roomIdentifier = "room_id"
|
static let roomIdentifier = "room_id"
|
||||||
static let eventIdentifier = "event_id"
|
static let eventIdentifier = "event_id"
|
||||||
static let unreadCount = "unread_count"
|
static let unreadCount = "unread_count"
|
||||||
|
static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
|
|||||||
@@ -52,15 +52,15 @@ struct NotificationItemProxy {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
"Title"
|
InfoPlistReader(bundle: .app).bundleDisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitle: String? {
|
var subtitle: String? {
|
||||||
nil
|
L10n.notification
|
||||||
}
|
}
|
||||||
|
|
||||||
var isNoisy: Bool {
|
var isNoisy: Bool {
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
var avatarURL: URL? {
|
var avatarURL: URL? {
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ class NotificationServiceProxy: NotificationServiceProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
|
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? {
|
||||||
nil
|
NotificationItemProxy()
|
||||||
// try await Task.dispatch(on: .global()) {
|
|
||||||
// guard let item = try self.service.getNotificationItem(roomId: roomId, eventId: eventId) else {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return .init(notificationItem: item)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,24 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RestorationToken: Codable, Equatable {
|
struct RestorationToken: Codable, Equatable {
|
||||||
let session: MatrixRustSDK.Session
|
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 {
|
extension MatrixRustSDK.Session: Codable {
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
|
guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
|
||||||
let roomId = request.roomId,
|
let roomId = request.roomId,
|
||||||
let eventId = request.eventId,
|
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:
|
// We cannot process this notification, it might be due to one of these:
|
||||||
// - Device rebooted and locked
|
// - Device rebooted and locked
|
||||||
// - Not a Matrix notification
|
// - Not a Matrix notification
|
||||||
// - User is not signed in
|
// - User is not signed in
|
||||||
|
// - NotificationID could not be resolved
|
||||||
return contentHandler(request.content)
|
return contentHandler(request.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ extension NotificationItemProxy {
|
|||||||
/// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
|
/// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
|
||||||
func process(with roomId: String,
|
func process(with roomId: String,
|
||||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
||||||
nil
|
|
||||||
// switch timelineItemProxy {
|
// switch timelineItemProxy {
|
||||||
// case .event(let eventItem):
|
// case .event(let eventItem):
|
||||||
// guard eventItem.isMessage else {
|
// guard eventItem.isMessage else {
|
||||||
@@ -73,6 +72,9 @@ extension NotificationItemProxy {
|
|||||||
// case .other:
|
// case .other:
|
||||||
// return nil
|
// 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
|
// MARK: - Private
|
||||||
|
|||||||
@@ -29,4 +29,8 @@ extension UNNotificationRequest {
|
|||||||
var unreadCount: Int? {
|
var unreadCount: Int? {
|
||||||
content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? 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.url, settings?.pushGatewayBaseURL.absoluteString)
|
||||||
XCTAssertEqual(data.format, .eventIdOnly)
|
XCTAssertEqual(data.format, .eventIdOnly)
|
||||||
let defaultPayload: [AnyHashable: Any] = [
|
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
|
||||||
"aps": [
|
alert: APSAlert(locKey: "Notification",
|
||||||
"mutable-content": 1,
|
locArgs: [])),
|
||||||
"alert": [
|
pusherNotificationClientIdentifier: nil)
|
||||||
"loc-key": "Notification",
|
XCTAssertEqual(data.defaultPayload, try defaultPayload.toJsonString())
|
||||||
"loc-args": []
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
XCTAssertEqual(data.defaultPayload, defaultPayload.jsonString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async throws {
|
func test_whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async throws {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Enabled Push Notifications with static text.
|
||||||
Reference in New Issue
Block a user