diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 45b98b0..b798e16 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -37,10 +37,14 @@ Grant access to Photo Library will allow Solian take photo or video for your post. NSMicrophoneUsageDescription Grant access to Photo Library will allow Solian record audio for your post. - NSPhotoLibraryUsageDescription - Grant access to Photo Library will allow Solian upload photo or video for your post. NSPhotoLibraryAddUsageDescription Grant access to Photo Library will allow Solian download photo to album for you. + NSPhotoLibraryUsageDescription + Grant access to Photo Library will allow Solian upload photo or video for your post. + NSUserActivityTypes + + INSendMessageIntent + UIApplicationSupportsIndirectInputEvents UIBackgroundModes @@ -49,6 +53,7 @@ remote-notification audio voip + processing UILaunchStoryboardName LaunchScreen @@ -62,10 +67,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSUserActivityTypes - - INSendMessageIntent - UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/ios/SolarNotifyService/NotificationService.swift b/ios/SolarNotifyService/NotificationService.swift index a78c80e..765ada1 100644 --- a/ios/SolarNotifyService/NotificationService.swift +++ b/ios/SolarNotifyService/NotificationService.swift @@ -23,6 +23,58 @@ class NotificationService: UNNotificationServiceExtension { identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)" } + private func fetchAvatarImage(from url: String, completion: @escaping (INImage?) -> Void) { + guard let imageURL = URL(string: url) else { + completion(nil) + return + } + + // Define a cache location based on the URL hash + let cacheFileName = imageURL.lastPathComponent + let tempDirectory = FileManager.default.temporaryDirectory + let cachedFileUrl = tempDirectory.appendingPathComponent(cacheFileName) + + // Check if the image is already cached + if FileManager.default.fileExists(atPath: cachedFileUrl.path) { + do { + let data = try Data(contentsOf: cachedFileUrl) + let cachedImage = INImage(imageData: data) // No optional binding here + completion(cachedImage) + return + } catch { + print("Failed to load cached avatar image: \(error.localizedDescription)") + try? FileManager.default.removeItem(at: cachedFileUrl) // Clear corrupted cache + } + } + + // Download the image if not cached + let session = URLSession(configuration: .default) + session.downloadTask(with: imageURL) { localUrl, response, error in + if let error = error { + print("Failed to fetch avatar image: \(error.localizedDescription)") + completion(nil) + return + } + + guard let localUrl = localUrl, let data = try? Data(contentsOf: localUrl) else { + print("Failed to fetch data for avatar image.") + completion(nil) + return + } + + do { + // Cache the downloaded file + try FileManager.default.moveItem(at: localUrl, to: cachedFileUrl) + } catch { + print("Failed to cache avatar image: \(error.localizedDescription)") + } + + // Create INImage from the downloaded data + let inImage = INImage(imageData: data) // Create directly + completion(inImage) + }.resume() + } + override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void @@ -61,32 +113,35 @@ class NotificationService: UNNotificationServiceExtension { throw ParseNotificationPayloadError.missingMetadata("The notification has no metadata.") } - guard var avatarUrl = metadata["avatar"] as? String else { + guard let avatarIdentifier = metadata["avatar"] as? String else { throw ParseNotificationPayloadError.missingAvatarUrl("The notification has no avatar.") } - avatarUrl = getAttachmentUrl(for: avatarUrl) - let handle = INPersonHandle(value: "\(metadata["user_id"] ?? "")", type: .unknown) - let avatar = INImage(url: URL(string: avatarUrl)!) - let sender = INPerson( - personHandle: handle, - nameComponents: nil, - displayName: content.title, - image: avatar, - contactIdentifier: nil, - customIdentifier: nil - ) - - if content.categoryIdentifier == "messaging.callStart" { - let intent = createCallIntent(with: sender) - donateInteraction(for: intent) - let updatedContent = try request.content.updating(from: intent) - contentHandler?(updatedContent) - } else { - let intent = createMessageIntent(with: sender, metadata: metadata, body: content.body) - donateInteraction(for: intent) - let updatedContent = try request.content.updating(from: intent) - contentHandler?(updatedContent) + let avatarUrl = getAttachmentUrl(for: avatarIdentifier) + fetchAvatarImage(from: avatarUrl) { [weak self] inImage in + guard let self = self else { return } + + let handle = INPersonHandle(value: "\(metadata["user_id"] ?? "")", type: .unknown) + let sender = INPerson( + personHandle: handle, + nameComponents: nil, + displayName: content.title, + image: inImage, + contactIdentifier: nil, + customIdentifier: nil + ) + + if content.categoryIdentifier == "messaging.callStart" { + let intent = self.createCallIntent(with: sender) + self.donateInteraction(for: intent) + let updatedContent = try? request.content.updating(from: intent) + self.contentHandler?(updatedContent ?? content) + } else { + let intent = self.createMessageIntent(with: sender, metadata: metadata, body: content.body) + self.donateInteraction(for: intent) + let updatedContent = try? request.content.updating(from: intent) + self.contentHandler?(updatedContent ?? content) + } } } @@ -106,9 +161,56 @@ class NotificationService: UNNotificationServiceExtension { private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String) { let attachmentUrl = getAttachmentUrl(for: identifier) - if let url = URL(string: attachmentUrl), let attachment = try? UNNotificationAttachment(identifier: identifier, url: url) { - content.attachments = [attachment] + + guard let remoteUrl = URL(string: attachmentUrl) else { + print("Invalid URL for attachment: \(attachmentUrl)") + return } + + // Define a cache location based on the identifier + let tempDirectory = FileManager.default.temporaryDirectory + let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) + + if FileManager.default.fileExists(atPath: cachedFileUrl.path) { + // Use cached file + attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) + } else { + // Download and cache the file + let session = URLSession(configuration: .default) + session.downloadTask(with: remoteUrl) { [weak content] localUrl, response, error in + guard let content = content else { return } + + if let error = error { + print("Failed to download media: \(error.localizedDescription)") + self.contentHandler?(content) + return + } + + guard let localUrl = localUrl else { + print("No local file URL after download") + self.contentHandler?(content) + return + } + + do { + // Move the downloaded file to the cache + try FileManager.default.moveItem(at: localUrl, to: cachedFileUrl) + self.attachLocalMedia(to: content, from: cachedFileUrl, withIdentifier: identifier) + } catch { + print("Failed to cache media file: \(error.localizedDescription)") + self.contentHandler?(content) + } + }.resume() + } + } + + private func attachLocalMedia(to content: UNMutableNotificationContent, from localUrl: URL, withIdentifier identifier: String) { + if let attachment = try? UNNotificationAttachment(identifier: identifier, url: localUrl) { + content.attachments = [attachment] + } else { + print("Failed to create attachment from cached file: \(localUrl.path)") + } + self.contentHandler?(content) } private func createCallIntent(with sender: INPerson) -> INStartCallIntent {