diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj
index 3a3d2b3a..512ce8a8 100644
--- a/IceCubesApp.xcodeproj/project.pbxproj
+++ b/IceCubesApp.xcodeproj/project.pbxproj
@@ -1203,7 +1203,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
};
name = Debug;
@@ -1238,7 +1238,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
VALIDATE_PRODUCT = YES;
};
diff --git a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme
index c53de172..06441acb 100644
--- a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme
+++ b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesNotifications.xcscheme
@@ -56,8 +56,12 @@
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
-
+
+
+
-
+
Void)?
+ override func didReceive(_ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+
+ let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
+ let provider = NotificationServiceContentProvider(bestAttemptContent: bestAttemptContent)
+ let casted = unsafeBitCast(contentHandler,
+ to: (@Sendable (UNNotificationContent) -> Void).self)
+ Task {
+ if let content = await provider.buildContent() {
+ casted(content)
+ }
+ }
+ }
+}
+
+actor NotificationServiceContentProvider {
var bestAttemptContent: UNMutableNotificationContent?
private let pushKeys = PushKeys()
-
- @MainActor
- override func didReceive(_ request: UNNotificationRequest,
- withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
- self.contentHandler = contentHandler
- bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
-
+ private let keychainAccounts = AppAccount.retrieveAll()
+
+ init(bestAttemptContent: UNMutableNotificationContent? = nil) {
+ self.bestAttemptContent = bestAttemptContent
+ }
+
+ func buildContent() async -> UNMutableNotificationContent? {
if var bestAttemptContent {
let privateKey = pushKeys.notificationsPrivateKeyAsKey
let auth = pushKeys.notificationsAuthKeyAsKey
-
+
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64())
else {
- contentHandler(bestAttemptContent)
- return
+ return bestAttemptContent
}
-
+
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
else {
- contentHandler(bestAttemptContent)
- return
+ return bestAttemptContent
}
-
+
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64())
else {
- contentHandler(bestAttemptContent)
- return
+ return bestAttemptContent
}
-
+
guard let plaintextData = NotificationService.decrypt(payload: payload,
salt: salt,
auth: auth,
privateKey: privateKey,
publicKey: publicKey),
- let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData)
+ let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData)
else {
- contentHandler(bestAttemptContent)
- return
+ return bestAttemptContent
}
-
+
bestAttemptContent.title = notification.title
- if AppAccountsManager.shared.availableAccounts.count > 1 {
+ if keychainAccounts.count > 1 {
bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? ""
}
bestAttemptContent.body = notification.body.escape()
bestAttemptContent.userInfo["plaintext"] = plaintextData
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf"))
-
- let preferences = UserPreferences.shared
- let tokens = AppAccountsManager.shared.pushAccounts.map(\.token)
- preferences.reloadNotificationsCount(tokens: tokens)
-
- if let token = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken {
- var currentCount = preferences.notificationsCount[token] ?? 0
- currentCount += 1
- preferences.notificationsCount[token] = currentCount
- }
-
- bestAttemptContent.badge = .init(integerLiteral: preferences.totalNotificationsCount)
-
+ let badgeCount = await updateBadgeCoung(notification: notification)
+ bestAttemptContent.badge = .init(integerLiteral: badgeCount)
+
if let urlString = notification.icon,
- let url = URL(string: urlString)
- {
+ let url = URL(string: urlString) {
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let filename = url.lastPathComponent
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
-
- Task {
- // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
- // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
- // boundary.
- // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
- // There is a Radar tracking this & others like it.
- if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
- if let image = UIImage(data: data) {
- try? image.pngData()?.write(to: fileURL)
-
- if let remoteNotification = await toRemoteNotification(localNotification: notification),
- let type = remoteNotification.supportedType
- {
- let intent = buildMessageIntent(remoteNotification: remoteNotification,
- currentUser: bestAttemptContent.userInfo["i"] as? String ?? "",
- avatarURL: fileURL)
+
+ // Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
+ // context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
+ // boundary.
+ // This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
+ // There is a Radar tracking this & others like it.
+ if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
+ if let image = UIImage(data: data) {
+ try? image.pngData()?.write(to: fileURL)
+
+ if let remoteNotification = await toRemoteNotification(localNotification: notification),
+ let type = remoteNotification.supportedType
+ {
+ let intent = buildMessageIntent(remoteNotification: remoteNotification,
+ currentUser: bestAttemptContent.userInfo["i"] as? String ?? "",
+ avatarURL: fileURL)
+ do {
bestAttemptContent = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
bestAttemptContent.threadIdentifier = remoteNotification.type
if type == .mention {
@@ -110,27 +112,32 @@ class NotificationService: UNNotificationServiceExtension {
let newBody = "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())"
bestAttemptContent.body = newBody
}
- } else {
- if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) {
- bestAttemptContent.attachments = [attachment]
- }
+ return bestAttemptContent
+ } catch {
+ return bestAttemptContent
+ }
+ } else {
+ if let attachment = try? UNNotificationAttachment(identifier: filename,
+ url: fileURL,
+ options: nil) {
+ bestAttemptContent.attachments = [attachment]
}
}
- contentHandler(bestAttemptContent)
- } else {
- contentHandler(bestAttemptContent)
}
+ } else {
+ return bestAttemptContent
}
} else {
- contentHandler(bestAttemptContent)
+ return bestAttemptContent
}
}
+ return nil
}
-
- @MainActor
+
+
private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? {
do {
- if let account = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) {
+ if let account = keychainAccounts.first(where: { $0.oauthToken?.accessToken == localNotification.accessToken }) {
let client = Client(server: account.server, oauthToken: account.oauthToken)
let remoteNotification: Models.Notification = try await client.get(endpoint: Notifications.notification(id: String(localNotification.notificationID)))
return remoteNotification
@@ -140,8 +147,7 @@ class NotificationService: UNNotificationServiceExtension {
}
return nil
}
-
- @MainActor
+
private func buildMessageIntent(remoteNotification: Models.Notification,
currentUser: String,
avatarURL: URL) -> INSendMessageIntent
@@ -156,7 +162,7 @@ class NotificationService: UNNotificationServiceExtension {
customIdentifier: nil)
var recipents: [INPerson]?
var groupName: INSpeakableString?
- if AppAccountsManager.shared.availableAccounts.count > 1 {
+ if keychainAccounts.count > 1 {
let me = INPerson(personHandle: .init(value: currentUser, type: .unknown),
nameComponents: nil,
displayName: currentUser,
@@ -179,4 +185,18 @@ class NotificationService: UNNotificationServiceExtension {
}
return intent
}
+
+ @MainActor
+ private func updateBadgeCoung(notification: MastodonPushNotification) -> Int {
+ let preferences = UserPreferences.shared
+ let tokens = AppAccountsManager.shared.pushAccounts.map(\.token)
+ preferences.reloadNotificationsCount(tokens: tokens)
+
+ if let token = keychainAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken {
+ var currentCount = preferences.notificationsCount[token] ?? 0
+ currentCount += 1
+ preferences.notificationsCount[token] = currentCount
+ }
+ return preferences.totalNotificationsCount
+ }
}
diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift
index 066d1684..54ce9bf5 100644
--- a/Packages/Env/Sources/Env/PushNotificationsService.swift
+++ b/Packages/Env/Sources/Env/PushNotificationsService.swift
@@ -111,14 +111,6 @@ public struct HandledNotification: Equatable {
UNUserNotificationCenter.current().delegate = self
}
- private var keychain: KeychainSwift {
- let keychain = KeychainSwift()
- #if !DEBUG && !targetEnvironment(simulator)
- keychain.accessGroup = AppInfo.keychainGroup
- #endif
- return keychain
- }
-
public func requestPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable _, _ in
DispatchQueue.main.async {