diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift index 069d65c47e..f63b7a1ddf 100644 --- a/modules/BlueskyNSE/NotificationService.swift +++ b/modules/BlueskyNSE/NotificationService.swift @@ -1,5 +1,6 @@ import UserNotifications import UIKit +import Intents let APP_GROUP = "group.app.bsky" typealias ContentHandler = (UNNotificationContent) -> Void @@ -32,22 +33,23 @@ class NotificationService: UNNotificationServiceExtension { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler - + guard let bestAttempt = NSEUtil.createCopy(request.content) else { contentHandler(request.content) return } - + self.bestAttempt = bestAttempt - + if isReasonDm() { - + } - + if isReasonDm() { mutateWithChatMessage(bestAttempt) - // contentHandler will be called within mutateWithAvatar's completion block - mutateWithAvatar(bestAttempt) + mutateWithAvatar(bestAttempt) { + contentHandler(bestAttempt) + } } else { mutateWithBadge(bestAttempt) contentHandler(bestAttempt) @@ -59,12 +61,12 @@ class NotificationService: UNNotificationServiceExtension { let bestAttempt = self.bestAttempt else { return } - + if isReasonDm(), let subject = self.getSubject() { NSEUtil.shared.cancelImageDownload(userId: subject) } - + contentHandler(bestAttempt) } @@ -94,40 +96,68 @@ class NotificationService: UNNotificationServiceExtension { func mutateWithDmSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) } - - func mutateWithAvatar(_ content: UNMutableNotificationContent) { + + func mutateWithAvatar(_ content: UNMutableNotificationContent, completion: @escaping () -> Void) { // Here, the server will only give us an avatar uri if there is a // follow relationship. All we need to check is the preference. - guard let avatarUrlString = content.userInfo["avatar"] as? String, - NSEUtil.shared.prefs?.bool(forKey: "showAvatarDms") == true, + guard NSEUtil.shared.prefs?.bool(forKey: "showAvatarDms") == true, + #available (iOSApplicationExtension 15.0, *), + let avatarUrlString = content.userInfo["avatar"] as? String, + let displayName = content.userInfo["displayName"] as? String, let avatarUrl = URL(string: avatarUrlString), - let subject = self.getSubject() else { + let subject = self.getSubject(), + let contentHandler = self.contentHandler else { + completion() return } - + NSEUtil.shared.downloadImageIfNecessary(avatarUrl, userId: subject) { imageUrl in - guard let contentHandler = self.contentHandler else { + guard let imageUrl = imageUrl else { + completion() return } - self.contentHandler?(content) + + let handle = INPersonHandle(value: subject, + type: .unknown) + let image = INImage(url: imageUrl) + let sender = INPerson(personHandle: handle, + nameComponents: nil, + displayName: displayName, + image: image, + contactIdentifier: subject, + customIdentifier: nil) + let intent = INSendMessageIntent.init(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: content.body, + speakableGroupName: nil, + conversationIdentifier: subject, + serviceName: nil, + sender: sender, + attachments: nil) + + _ = try? content.updating(from: intent) + completion() } } - + // MARK: Util - + func isReasonDm() -> Bool { guard let reason = self.bestAttempt?.userInfo["reason"] as? String else { return false } return reason == "chat-message" } - + func getSubject() -> String? { guard let subject = self.bestAttempt?.userInfo["subject"] as? String else { return nil } return subject } + + func addPersonAndAvatar(_ content: UNMutableNotificationContent) { + } } // NSEUtil's purpose is to create a shared instance of `UserDefaults` across @@ -136,41 +166,41 @@ class NotificationService: UNNotificationServiceExtension { private class NSEUtil { static let shared = NSEUtil() - + var prefs = UserDefaults(suiteName: APP_GROUP) var prefsQueue = DispatchQueue(label: "NSEPrefsQueue") - + // Store each image download task that private let imageDownloadTasks = NSMapTable( keyOptions: NSPointerFunctions.Options.weakMemory, valueOptions: NSPointerFunctions.Options.weakMemory ) - + // Creates a mutable copy of the provided content static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { return content.mutableCopy() as? UNMutableNotificationContent } - + private func createFileURL(userId: String) -> URL? { let dir = FileManager.default.temporaryDirectory return URL(string: "\(dir.absoluteString)\(userId).png") } - + private func imageExists(_ url: URL) -> Bool { return FileManager.default.fileExists(atPath: url.absoluteString) } - + func downloadImageIfNecessary(_ imageUrl: URL, userId: String, completion: @escaping (URL?) -> Void) { guard let outUrl = self.createFileURL(userId: userId) else { completion(nil) return } - - if (self.imageExists(outUrl)) { + + if self.imageExists(outUrl) { completion(outUrl) return } - + let task = ImageDownloadTask(imageUrl: imageUrl) { image in guard let image = image, let pngData = image.pngData() else { @@ -186,7 +216,7 @@ private class NSEUtil { } Self.shared.imageDownloadTasks.setObject(task, forKey: NSString(string: userId)) } - + func cancelImageDownload(userId: String) { guard let imageDownloadTask = self.imageDownloadTasks.object(forKey: NSString(string: userId)) else { return @@ -201,12 +231,12 @@ private class NSEUtil { class ImageDownloadTask { private var completionBlocks = [ImageDownloadTaskCompletion]() private var task: URLSessionDataTask? - + init(imageUrl: URL, completion: @escaping ImageDownloadTaskCompletion) { self.completionBlocks.append(completion) self.createTask(imageUrl) } - + private func createTask(_ imageUrl: URL) { let task = URLSession.shared.dataTask(with: imageUrl) { data, _, _ in guard let data = data, @@ -218,17 +248,17 @@ class ImageDownloadTask { } task.resume() } - + private func callEachCompletionBlock(_ image: UIImage?) { completionBlocks.forEach { completion in completion(image) } } - + func addCompletionBlock(_ completion: @escaping ImageDownloadTaskCompletion) { self.completionBlocks.append(completion) } - + func cancel() { self.task?.cancel() self.callEachCompletionBlock(nil)