From ca14631487a0336528c47ea5faa7b787dd46e6cf Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 27 Jan 2023 22:14:43 +0200 Subject: [PATCH] Fixes crashes and errors in iOS Share Extension and Notification Service (#7032) * Fix erros & crashes in iOS share extension * Fix erros & crashes in iOS notification service --------- Co-authored-by: Mattermost Build --- .../xcshareddata/xcschemes/Gekidou.xcscheme | 67 +++++++++++ ios/Gekidou/Sources/Gekidou/FileCache.swift | 7 +- ios/Gekidou/Sources/Gekidou/Keychain.swift | 9 +- .../Gekidou/Networking/PushNotification.swift | 94 +++++++-------- .../Networking/ShareExtension+Post.swift | 10 +- ios/Mattermost.xcodeproj/project.pbxproj | 4 + ios/MattermostShare/Extensions/Color.swift | 1 + ios/MattermostShare/ShareViewController.swift | 8 +- .../ViewModels/ShareViewModel.swift | 6 +- .../Views/ErrorViews/ErrorSharingView.swift | 54 +++++++++ ios/MattermostShare/Views/InitialView.swift | 14 ++- .../Views/NavigationButtons/PostButton.swift | 5 + .../NotificationService.swift | 112 ++++++++++-------- 13 files changed, 280 insertions(+), 111 deletions(-) create mode 100644 ios/Gekidou/.swiftpm/xcode/xcshareddata/xcschemes/Gekidou.xcscheme create mode 100644 ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift diff --git a/ios/Gekidou/.swiftpm/xcode/xcshareddata/xcschemes/Gekidou.xcscheme b/ios/Gekidou/.swiftpm/xcode/xcshareddata/xcschemes/Gekidou.xcscheme new file mode 100644 index 0000000000..3f997e5f39 --- /dev/null +++ b/ios/Gekidou/.swiftpm/xcode/xcshareddata/xcschemes/Gekidou.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Gekidou/Sources/Gekidou/FileCache.swift b/ios/Gekidou/Sources/Gekidou/FileCache.swift index 8e0cf05a76..118441fcc5 100644 --- a/ios/Gekidou/Sources/Gekidou/FileCache.swift +++ b/ios/Gekidou/Sources/Gekidou/FileCache.swift @@ -51,11 +51,10 @@ public class FileCache: NSObject { } public func saveProfileImage(serverUrl: String, userId: String, imageData: Data?) { - guard let data = imageData, - let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) - else { return } - do { + guard let data = imageData, + let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) + else { return } try data.write(to: url) } catch let error { print("Erro saving image. \(error)") diff --git a/ios/Gekidou/Sources/Gekidou/Keychain.swift b/ios/Gekidou/Sources/Gekidou/Keychain.swift index 571811cc61..a31f7aeb95 100644 --- a/ios/Gekidou/Sources/Gekidou/Keychain.swift +++ b/ios/Gekidou/Sources/Gekidou/Keychain.swift @@ -48,6 +48,7 @@ extension KeychainError: LocalizedError { public class Keychain: NSObject { @objc public static let `default` = Keychain() + private var tokenCache = Dictionary() public func getClientIdentityAndCertificate(for host: String) throws -> (SecIdentity, SecCertificate)? { let query = try buildIdentityQuery(for: host) @@ -80,6 +81,10 @@ public class Keychain: NSObject { } public func getToken(for serverUrl: String) throws -> String? { + if let cache = tokenCache[serverUrl] { + return cache + } + var attributes = try buildTokenAttributes(for: serverUrl) attributes[kSecMatchLimit] = kSecMatchLimitOne attributes[kSecReturnData] = kCFBooleanTrue @@ -88,7 +93,9 @@ public class Keychain: NSObject { let status = SecItemCopyMatching(attributes as CFDictionary, &result) let data = result as? Data if status == errSecSuccess && data != nil { - return String(data: data!, encoding: .utf8) + let token = String(data: data!, encoding: .utf8) + tokenCache[serverUrl] = token + return token } return nil diff --git a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift index 596e17c101..415bc7b246 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift @@ -87,25 +87,22 @@ extension Network { private func matchUsername(in message: String) -> [String] { let specialMentions = Set(["all", "here", "channel"]) - do { - let regex = try NSRegularExpression(pattern: "\\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)", options: [.caseInsensitive]) + if let regex = try? NSRegularExpression(pattern: "\\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)", options: [.caseInsensitive]) { let results = regex.matches(in: message, range: _NSRange(message.startIndex..., in: message)) - return results.map{ String(message[Range($0.range, in: message)!]).removePrefix("@") }.filter{ !specialMentions.contains($0)} - } catch let error { - print("invalid regex: \(error.localizedDescription)") - return [] + if !results.isEmpty { + let username = results.map({ String(message[Range($0.range, in: message)!]).removePrefix("@") }).filter({ !specialMentions.contains($0)}) + return username + } } + return [] } - public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?) -> Data? { - var imgData: Data? - let semaphore = DispatchSemaphore(value: 0) - + public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) { func processResponse(data: Data?, response: URLResponse?, error: Error?) { if let httpResponse = response as? HTTPURLResponse { if (httpResponse.statusCode == 200 && error == nil) { - imgData = data FileCache.default.saveProfileImage(serverUrl: serverUrl, userId: senderId, imageData: data) + completionHandler(data) } else { os_log( OSLogType.default, @@ -115,7 +112,6 @@ extension Network { ) } } - semaphore.signal() } if let overrideUrl = overrideIconUrl, @@ -124,16 +120,12 @@ extension Network { } else { if let image = FileCache.default.getProfileImage(serverUrl: serverUrl, userId: senderId) { os_log(OSLogType.default, "Mattermost Notifications: cached image") - imgData = image.pngData() - semaphore.signal() + completionHandler(image.pngData()) } else { os_log(OSLogType.default, "Mattermost Notifications: image not cached") fetchUserProfilePicture(userId: senderId, withServerUrl: serverUrl, completionHandler: processResponse) } } - - semaphore.wait() - return imgData } public func postNotificationReceipt(_ ackNotification: AckNotification, completionHandler: @escaping ResponseHandler) { @@ -152,14 +144,20 @@ extension Network { let operation = BlockOperation { let group = DispatchGroup() - let channelId = notification.userInfo["channel_id"] as! String - let rootId = notification.userInfo.index(forKey: "root_id") != nil ? notification.userInfo["root_id"] as! String : "" - let serverUrl = notification.userInfo["server_url"] as! String - let isCRTEnabled = notification.userInfo["is_crt_enabled"] as! Bool - let currentUser = try! Database.default.queryCurrentUser(serverUrl) + let channelId = notification.userInfo["channel_id"] as? String + let rootId = notification.userInfo["root_id"] as? String ?? "" + let serverUrl = notification.userInfo["server_url"] as? String + let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false + + guard let serverUrl = serverUrl, + let channelId = channelId + else { return } + + let currentUser = try? Database.default.queryCurrentUser(serverUrl) let currentUserId = currentUser?[Expression("id")] let currentUsername = currentUser?[Expression("username")] + var postData: PostData? = nil var myChannelData: ChannelMemberData? = nil var threadData: ThreadData? = nil @@ -196,9 +194,10 @@ extension Network { group.enter() let since = try? Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl) self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl, withIsCRTEnabled: isCRTEnabled, withRootId: rootId) { data, response, error in - if self.responseOK(response), let data = data { - postData = try! JSONDecoder().decode(PostData.self, from: data) - if postData?.posts.count ?? 0 > 0 { + if self.responseOK(response), let data = data, + let jsonData = try? JSONDecoder().decode(PostData.self, from: data) { + postData = jsonData + if jsonData.posts.count > 0 { var authorIds: Set = Set() var usernames: Set = Set() @@ -206,7 +205,7 @@ extension Network { var threadParticipantUsernames: Set = Set() // Used to exclude the "usernames" present in the thread participants var threadParticipantUsers = [String: User]() // All unique users from thread participants are stored here - postData!.posts.forEach{post in + jsonData.posts.forEach{post in if (currentUserId != nil && post.user_id != currentUserId) { authorIds.insert(post.user_id) } @@ -304,27 +303,28 @@ extension Network { group.wait() group.enter() - if (postData != nil && postData?.posts != nil && postData!.posts.count > 0) { - if let db = try? Database.default.getDatabaseForServer(serverUrl) { - let receivingThreads = isCRTEnabled && !rootId.isEmpty - try? db.transaction { - try? Database.default.handlePostData(db, postData!, channelId, since != nil, receivingThreads) - - if threads.count > 0 { - try? Database.default.handleThreads(db, threads) - } - - if users.count > 0 { - try? Database.default.insertUsers(db, users) - } - - if myChannelData != nil { - try? Database.default.handleMyChannelMentions(db, myChannelData!, withCRTEnabled: isCRTEnabled) - } - - if threadData != nil { - try? Database.default.handleThreadMentions(db, threadData!) - } + if let data = postData, + let posts = data.posts as [Post]?, + let db = try? Database.default.getDatabaseForServer(serverUrl), + posts.count > 0 { + let receivingThreads = isCRTEnabled && !rootId.isEmpty + try? db.transaction { + try? Database.default.handlePostData(db, data, channelId, since != nil, receivingThreads) + + if threads.count > 0 { + try? Database.default.handleThreads(db, threads) + } + + if users.count > 0 { + try? Database.default.insertUsers(db, users) + } + + if let myChannel = myChannelData { + try? Database.default.handleMyChannelMentions(db, myChannel, withCRTEnabled: isCRTEnabled) + } + + if let threads = threadData { + try? Database.default.handleThreadMentions(db, threads) } } } diff --git a/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift b/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift index 70d762c209..70692a4dd5 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift @@ -10,7 +10,7 @@ import os.log extension ShareExtension { public func uploadFiles(serverUrl: String, channelId: String, message: String, -files: [String], completionHandler: @escaping () -> Void) { +files: [String], completionHandler: @escaping () -> Void) -> String? { let id = "mattermost-share-upload-\(UUID().uuidString)" createUploadSessionData( @@ -18,6 +18,8 @@ files: [String], completionHandler: @escaping () -> Void) { channelId: channelId, message: message, files: files ) + + guard let token = try? Keychain.default.getToken(for: serverUrl) else {return "Could not retrieve the session token from the KeyChain"} if !files.isEmpty { createBackroundSession(id: id) @@ -26,8 +28,7 @@ files: [String], completionHandler: @escaping () -> Void) { fileUrl.isFileURL { let filename = fileUrl.lastPathComponent - if let url = URL(string: "\(serverUrl)/api/v4/files?channel_id=\(channelId)&filename=\(filename)"), - let token = try? Keychain.default.getToken(for: serverUrl) { + if let url = URL(string: "\(serverUrl)/api/v4/files?channel_id=\(channelId)&filename=\(filename)") { var uploadRequest = URLRequest(url: url) uploadRequest.httpMethod = "POST" uploadRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") @@ -39,7 +40,6 @@ files: [String], completionHandler: @escaping () -> Void) { task.resume() } } - } } completionHandler() @@ -51,6 +51,8 @@ files: [String], completionHandler: @escaping () -> Void) { ) self.postMessageForSession(withId: id, completionHandler: completionHandler) } + + return nil } func postMessageForSession(withId id: String, completionHandler: (() -> Void)? = nil) { diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 8a68311f99..c675279149 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 7FD482692864DC5900A5B18B /* OpenSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DB14DFDF6E04FA69FE769DC /* OpenSans-SemiBoldItalic.ttf */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC870028A4325D00DE96CB /* NotificationsModule.m */; }; + 7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */; }; A94508A396424B2DB778AFE9 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; }; /* End PBXBuildFile section */ @@ -251,6 +252,7 @@ 7FEB109A1F61019C0039A015 /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MattermostManaged.m; path = Mattermost/MattermostManaged.m; sourceTree = ""; }; 7FEC870028A4325D00DE96CB /* NotificationsModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NotificationsModule.m; path = Mattermost/NotificationsModule.m; sourceTree = ""; }; 7FEC870428A44A7B00DE96CB /* NotificationsModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NotificationsModule.h; path = Mattermost/NotificationsModule.h; sourceTree = ""; }; + 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorSharingView.swift; sourceTree = ""; }; 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -604,6 +606,7 @@ 7F93AAA528761F2E0047B89F /* NoServersView.swift */, 7F93AAA728761FA10047B89F /* NoMembershipView.swift */, 7F93AAAB28771E3F0047B89F /* ErrorLabelView.swift */, + 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */, ); path = ErrorViews; sourceTree = ""; @@ -1046,6 +1049,7 @@ 7F7E9F5E2864E76D0064BFAF /* SingleAttachmentView.swift in Sources */, 7F7E9F472864E6C60064BFAF /* View.swift in Sources */, 7F93AAAE287725660047B89F /* Int64.swift in Sources */, + 7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */, 7F93AAB8287778090047B89F /* Publishers.swift in Sources */, 7F7E9F732864E8060064BFAF /* CompassIcons.swift in Sources */, 7F93AABA28777A390047B89F /* Notification.swift in Sources */, diff --git a/ios/MattermostShare/Extensions/Color.swift b/ios/MattermostShare/Extensions/Color.swift index 1306c28cbb..451d93a395 100644 --- a/ios/MattermostShare/Extensions/Color.swift +++ b/ios/MattermostShare/Extensions/Color.swift @@ -42,6 +42,7 @@ extension Color { struct ColorTheme { let awayIndicator = Color("awayIndicator") let buttonBg = Color("buttonBg") + let buttonColor = Color("buttonColor") let centerChannelBg = Color("centerChannelBg") let centerChannelColor = Color("centerChannelColor") let dndIndicator = Color("dndIndicator") diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift index e70489dd1a..c9eacaa82b 100644 --- a/ios/MattermostShare/ShareViewController.swift +++ b/ios/MattermostShare/ShareViewController.swift @@ -87,7 +87,6 @@ class ShareViewController: UIViewController { } private func doPost(_ notification: Notification) { - removeObservers() if let userInfo = notification.userInfo { let serverUrl = userInfo["serverUrl"] as? String let channelId = userInfo["channelId"] as? String @@ -121,15 +120,20 @@ class ShareViewController: UIViewController { ) let shareExtension = Gekidou.ShareExtension() - shareExtension.uploadFiles( + let uploadError = shareExtension.uploadFiles( serverUrl: url, channelId: channel, message: message, files: files, completionHandler: { [weak self] in + self?.removeObservers() self?.extensionContext!.completeRequest(returningItems: []) }) + if uploadError != nil { + NotificationCenter.default.post(name: Notification.Name("errorPosting"), object: nil, userInfo: ["info": uploadError as Any]) + } } else { + removeObservers() extensionContext!.completeRequest(returningItems: []) } } diff --git a/ios/MattermostShare/ViewModels/ShareViewModel.swift b/ios/MattermostShare/ViewModels/ShareViewModel.swift index fe46aa9652..ee643381b3 100644 --- a/ios/MattermostShare/ViewModels/ShareViewModel.swift +++ b/ios/MattermostShare/ViewModels/ShareViewModel.swift @@ -87,12 +87,14 @@ class ShareViewModel: ObservableObject { func downloadProfileImage(serverUrl: String, userId: String, imageBinding: Binding) { guard let _ = URL(string: serverUrl) else { - fatalError("Missing or Malformed URL") + debugPrint("Missing or Malformed URL") + return } Gekidou.Network.default.fetchUserProfilePicture(userId: userId, withServerUrl: serverUrl, completionHandler: {data, response, error in guard (response as? HTTPURLResponse)?.statusCode == 200 else { - fatalError("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))") + debugPrint("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))") + return } if let data = data { diff --git a/ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift b/ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift new file mode 100644 index 0000000000..beb9e3c910 --- /dev/null +++ b/ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift @@ -0,0 +1,54 @@ +// +// ErrorSharingView.swift +// MattermostShare +// +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// + +import SwiftUI + +struct ErrorSharingView: View { + var error: String + @State var retrying = false + + let onError = NotificationCenter.default.publisher(for: Notification.Name("errorPosting")) + + var body: some View { + VStack (spacing: 8) { + if retrying { + ProgressView() + .onAppear { + NotificationCenter.default.post(name: Notification.Name("submit"), object: nil, userInfo: nil) + } + } else { + Text("An error ocurred") + .font(Font.custom("Metropolis-SemiBold", size: 20)) + .foregroundColor(Color.theme.centerChannelColor) + Text("There was an error when attempting to share the content to Mattermost.") + .font(Font.custom("OpenSans", size: 16)) + .foregroundColor(Color.theme.centerChannelColor.opacity(0.72)) + Text("Reason: \(error)") + .font(Font.custom("OpenSans", size: 12)) + .foregroundColor(Color.theme.centerChannelColor.opacity(0.60)) + Button { + retrying = true + } label: { + Text("Try again") + .font(Font.custom("OpenSans", size: 16)) + .foregroundColor(Color.theme.buttonColor) + } + .buttonStyle(.borderedProminent) + .tint(Color.theme.buttonBg) + } + } + .transition(.opacity) + .animation(.linear(duration: 0.3)) + .padding(.horizontal, 12) + .onReceive(onError) {_ in + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + retrying = false + } + } + } +} diff --git a/ios/MattermostShare/Views/InitialView.swift b/ios/MattermostShare/Views/InitialView.swift index 7b075d7bc7..daee572332 100644 --- a/ios/MattermostShare/Views/InitialView.swift +++ b/ios/MattermostShare/Views/InitialView.swift @@ -14,6 +14,9 @@ struct InitialView: View { @Binding var attachments: [AttachmentModel] @Binding var linkPreviewUrl: String @Binding var message: String + @State var error: String? + + let onError = NotificationCenter.default.publisher(for: Notification.Name("errorPosting")) var noServers: Bool { shareViewModel.allServers.count == 0 || shareViewModel.server == nil @@ -42,7 +45,10 @@ struct InitialView: View { var body: some View { return VStack { - if noServers { + if error != nil { + ErrorSharingView(error: error!) + .transition(.opacity) + } else if noServers { NoServersView() } else if noChannels { NoMembershipView() @@ -65,5 +71,11 @@ struct InitialView: View { ) ) .padding(20) + .animation(.linear(duration: 0.3)) + .onReceive(onError) {obj in + if let userInfo = obj.userInfo, let info = userInfo["info"] as? String { + error = info + } + } } } diff --git a/ios/MattermostShare/Views/NavigationButtons/PostButton.swift b/ios/MattermostShare/Views/NavigationButtons/PostButton.swift index 4b4f4e7ef0..b6d3bd186c 100644 --- a/ios/MattermostShare/Views/NavigationButtons/PostButton.swift +++ b/ios/MattermostShare/Views/NavigationButtons/PostButton.swift @@ -15,6 +15,8 @@ struct PostButton: View { @Binding var message: String @State var pressed: Bool = false + let submitPublisher = NotificationCenter.default.publisher(for: Notification.Name("submit")) + func submit() { let userInfo: [String: Any] = [ "serverUrl": shareViewModel.server?.url as Any, @@ -50,5 +52,8 @@ struct PostButton: View { .padding(.leading, 10) .padding(.vertical, 5) .disabled(disabled || pressed) + .onReceive(submitPublisher) {_ in + submit() + } } } diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 7e107ca269..700472a58e 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -81,70 +81,82 @@ class NotificationService: UNNotificationServiceExtension { } } - func sendMessageIntent(notification: UNNotificationContent) { - if #available(iOSApplicationExtension 15.0, *) { - let isCRTEnabled = notification.userInfo["is_crt_enabled"] as! Bool - let channelId = notification.userInfo["channel_id"] as! String - let rootId = notification.userInfo.index(forKey: "root_id") != nil ? notification.userInfo["root_id"] as! String : "" + func sendMessageIntentCompletion(_ notification: UNNotificationContent, _ avatarData: Data?) { + if #available(iOSApplicationExtension 15.0, *), + let imgData = avatarData, + let channelId = notification.userInfo["channel_id"] as? String { + os_log(OSLogType.default, "Mattermost Notifications: creating intent") + + let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false + let rootId = notification.userInfo["root_id"] as? String ?? "" let senderId = notification.userInfo["sender_id"] as? String ?? "" let channelName = notification.userInfo["channel_name"] as? String ?? "" - let overrideIconUrl = notification.userInfo["override_icon_url"] as? String - let serverUrl = notification.userInfo["server_url"] as? String ?? "" let message = (notification.userInfo["message"] as? String ?? "") + let avatar = INImage(imageData: imgData) as INImage? - if senderId != "" && serverUrl != "" { - os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, senderId) - let avatarData = Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl) - if let imgData = avatarData, - let avatar = INImage(imageData: imgData) as INImage? { - os_log(OSLogType.default, "Mattermost Notifications: creating intent") - var conversationId = channelId - if isCRTEnabled && rootId != "" { - conversationId = rootId - } + var conversationId = channelId + if isCRTEnabled && !rootId.isEmpty { + conversationId = rootId + } - let handle = INPersonHandle(value: senderId, type: .unknown) - let sender = INPerson(personHandle: handle, - nameComponents: nil, - displayName: channelName, - image: avatar, - contactIdentifier: nil, - customIdentifier: nil) + let handle = INPersonHandle(value: senderId, type: .unknown) + let sender = INPerson(personHandle: handle, + nameComponents: nil, + displayName: channelName, + image: avatar, + contactIdentifier: nil, + customIdentifier: nil) - let intent = INSendMessageIntent(recipients: nil, - outgoingMessageType: .outgoingMessageText, - content: message, - speakableGroupName: nil, - conversationIdentifier: conversationId, - serviceName: nil, - sender: sender, - attachments: nil) + let intent = INSendMessageIntent(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: message, + speakableGroupName: nil, + conversationIdentifier: conversationId, + serviceName: nil, + sender: sender, + attachments: nil) - let interaction = INInteraction(intent: intent, response: nil) - interaction.direction = .incoming - interaction.donate { error in - if error != nil { - self.contentHandler?(notification) - os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg) - } - - do { - let updatedContent = try notification.updating(from: intent) - os_log(OSLogType.default, "Mattermost Notifications: present updated notification") - self.contentHandler?(updatedContent) - } catch { - os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg) - self.contentHandler?(notification) - } - } + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + interaction.donate { error in + if error != nil { + self.contentHandler?(notification) + os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg) + } + + do { + let updatedContent = try notification.updating(from: intent) + os_log(OSLogType.default, "Mattermost Notifications: present updated notification") + self.contentHandler?(updatedContent) + } catch { + os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg) + self.contentHandler?(notification) } } } else { - os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification") self.contentHandler?(notification) } } + func sendMessageIntent(notification: UNNotificationContent) { + if #available(iOSApplicationExtension 15.0, *) { + guard let serverUrl = notification.userInfo["server_url"] as? String, + let senderId = notification.userInfo["sender_id"] as? String + else { + os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification") + self.contentHandler?(notification) + return + } + + os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, senderId) + let overrideIconUrl = notification.userInfo["override_icon_url"] as? String + + Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl) {[weak self] data in + self?.sendMessageIntentCompletion(notification, data) + } + } + } + func fetchReceipt(_ ackNotification: AckNotification) -> Void { if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) { os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")