mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 15:40:37 +00:00
Fix push notification
This commit is contained in:
parent
864cc26e08
commit
fca76849bc
4 changed files with 98 additions and 82 deletions
|
@ -1203,7 +1203,7 @@
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
|
@ -1238,7 +1238,7 @@
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,8 +56,12 @@
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
allowLocationSimulation = "YES"
|
allowLocationSimulation = "YES"
|
||||||
launchAutomaticallySubstyle = "2">
|
launchAutomaticallySubstyle = "2">
|
||||||
<BuildableProductRunnable
|
<RemoteRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "1"
|
||||||
|
BundleIdentifier = "com.thomasricouard.IceCubesApp"
|
||||||
|
RemotePath = "/Users/dimillian/Library/Developer/CoreSimulator/Devices/8EF923D0-4CF1-49B6-B287-5F05AD5440C1/data/Containers/Bundle/Application/C447A1D1-9BC9-49C9-8FA5-130E8403972F/Ice Cubes.app">
|
||||||
|
</RemoteRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||||
|
@ -65,7 +69,7 @@
|
||||||
BlueprintName = "IceCubesApp"
|
BlueprintName = "IceCubesApp"
|
||||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</MacroExpansion>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|
|
@ -9,18 +9,35 @@ import Notifications
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
extension UNMutableNotificationContent: @unchecked @retroactive Sendable { }
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
var contentHandler: ((UNNotificationContent) -> 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?
|
var bestAttemptContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
private let pushKeys = PushKeys()
|
private let pushKeys = PushKeys()
|
||||||
|
private let keychainAccounts = AppAccount.retrieveAll()
|
||||||
|
|
||||||
@MainActor
|
init(bestAttemptContent: UNMutableNotificationContent? = nil) {
|
||||||
override func didReceive(_ request: UNNotificationRequest,
|
self.bestAttemptContent = bestAttemptContent
|
||||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
}
|
||||||
self.contentHandler = contentHandler
|
|
||||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
|
||||||
|
|
||||||
|
func buildContent() async -> UNMutableNotificationContent? {
|
||||||
if var bestAttemptContent {
|
if var bestAttemptContent {
|
||||||
let privateKey = pushKeys.notificationsPrivateKeyAsKey
|
let privateKey = pushKeys.notificationsPrivateKeyAsKey
|
||||||
let auth = pushKeys.notificationsAuthKeyAsKey
|
let auth = pushKeys.notificationsAuthKeyAsKey
|
||||||
|
@ -28,23 +45,20 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
|
||||||
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64())
|
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64())
|
||||||
else {
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
||||||
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
|
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
|
||||||
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
|
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
|
||||||
else {
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
||||||
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64())
|
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64())
|
||||||
else {
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let plaintextData = NotificationService.decrypt(payload: payload,
|
guard let plaintextData = NotificationService.decrypt(payload: payload,
|
||||||
|
@ -54,39 +68,26 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
publicKey: publicKey),
|
publicKey: publicKey),
|
||||||
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData)
|
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData)
|
||||||
else {
|
else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bestAttemptContent.title = notification.title
|
bestAttemptContent.title = notification.title
|
||||||
if AppAccountsManager.shared.availableAccounts.count > 1 {
|
if keychainAccounts.count > 1 {
|
||||||
bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? ""
|
bestAttemptContent.subtitle = bestAttemptContent.userInfo["i"] as? String ?? ""
|
||||||
}
|
}
|
||||||
bestAttemptContent.body = notification.body.escape()
|
bestAttemptContent.body = notification.body.escape()
|
||||||
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
||||||
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf"))
|
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "glass.caf"))
|
||||||
|
let badgeCount = await updateBadgeCoung(notification: notification)
|
||||||
let preferences = UserPreferences.shared
|
bestAttemptContent.badge = .init(integerLiteral: badgeCount)
|
||||||
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)
|
|
||||||
|
|
||||||
if let urlString = notification.icon,
|
if let urlString = notification.icon,
|
||||||
let url = URL(string: urlString)
|
let url = URL(string: urlString) {
|
||||||
{
|
|
||||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
|
||||||
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
let filename = url.lastPathComponent
|
let filename = url.lastPathComponent
|
||||||
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
||||||
|
|
||||||
Task {
|
|
||||||
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
||||||
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
||||||
// boundary.
|
// boundary.
|
||||||
|
@ -102,6 +103,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
let intent = buildMessageIntent(remoteNotification: remoteNotification,
|
let intent = buildMessageIntent(remoteNotification: remoteNotification,
|
||||||
currentUser: bestAttemptContent.userInfo["i"] as? String ?? "",
|
currentUser: bestAttemptContent.userInfo["i"] as? String ?? "",
|
||||||
avatarURL: fileURL)
|
avatarURL: fileURL)
|
||||||
|
do {
|
||||||
bestAttemptContent = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
|
bestAttemptContent = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
|
||||||
bestAttemptContent.threadIdentifier = remoteNotification.type
|
bestAttemptContent.threadIdentifier = remoteNotification.type
|
||||||
if type == .mention {
|
if type == .mention {
|
||||||
|
@ -110,27 +112,32 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
let newBody = "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())"
|
let newBody = "\(NSLocalizedString(type.notificationKey(), bundle: .main, comment: ""))\(notification.body.escape())"
|
||||||
bestAttemptContent.body = newBody
|
bestAttemptContent.body = newBody
|
||||||
}
|
}
|
||||||
|
return bestAttemptContent
|
||||||
|
} catch {
|
||||||
|
return bestAttemptContent
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) {
|
if let attachment = try? UNNotificationAttachment(identifier: filename,
|
||||||
|
url: fileURL,
|
||||||
|
options: nil) {
|
||||||
bestAttemptContent.attachments = [attachment]
|
bestAttemptContent.attachments = [attachment]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentHandler(bestAttemptContent)
|
|
||||||
} else {
|
} else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentHandler(bestAttemptContent)
|
return bestAttemptContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? {
|
private func toRemoteNotification(localNotification: MastodonPushNotification) async -> Models.Notification? {
|
||||||
do {
|
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 client = Client(server: account.server, oauthToken: account.oauthToken)
|
||||||
let remoteNotification: Models.Notification = try await client.get(endpoint: Notifications.notification(id: String(localNotification.notificationID)))
|
let remoteNotification: Models.Notification = try await client.get(endpoint: Notifications.notification(id: String(localNotification.notificationID)))
|
||||||
return remoteNotification
|
return remoteNotification
|
||||||
|
@ -141,7 +148,6 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func buildMessageIntent(remoteNotification: Models.Notification,
|
private func buildMessageIntent(remoteNotification: Models.Notification,
|
||||||
currentUser: String,
|
currentUser: String,
|
||||||
avatarURL: URL) -> INSendMessageIntent
|
avatarURL: URL) -> INSendMessageIntent
|
||||||
|
@ -156,7 +162,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
customIdentifier: nil)
|
customIdentifier: nil)
|
||||||
var recipents: [INPerson]?
|
var recipents: [INPerson]?
|
||||||
var groupName: INSpeakableString?
|
var groupName: INSpeakableString?
|
||||||
if AppAccountsManager.shared.availableAccounts.count > 1 {
|
if keychainAccounts.count > 1 {
|
||||||
let me = INPerson(personHandle: .init(value: currentUser, type: .unknown),
|
let me = INPerson(personHandle: .init(value: currentUser, type: .unknown),
|
||||||
nameComponents: nil,
|
nameComponents: nil,
|
||||||
displayName: currentUser,
|
displayName: currentUser,
|
||||||
|
@ -179,4 +185,18 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
}
|
}
|
||||||
return intent
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,14 +111,6 @@ public struct HandledNotification: Equatable {
|
||||||
UNUserNotificationCenter.current().delegate = self
|
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() {
|
public func requestPushNotifications() {
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable _, _ in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable _, _ in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
Loading…
Reference in a new issue