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 <build@mattermost.com>
This commit is contained in:
Elias Nahum
2023-01-27 22:14:43 +02:00
committed by GitHub
parent a535728d5c
commit ca14631487
13 changed files with 280 additions and 111 deletions

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Gekidou"
BuildableName = "Gekidou"
BlueprintName = "Gekidou"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Gekidou"
BuildableName = "Gekidou"
BlueprintName = "Gekidou"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -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)")

View File

@@ -48,6 +48,7 @@ extension KeychainError: LocalizedError {
public class Keychain: NSObject {
@objc public static let `default` = Keychain()
private var tokenCache = Dictionary<String, String>()
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

View File

@@ -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<String>("id")]
let currentUsername = currentUser?[Expression<String>("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<String> = Set()
var usernames: Set<String> = Set()
@@ -206,7 +205,7 @@ extension Network {
var threadParticipantUsernames: Set<String> = 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)
}
}
}

View File

@@ -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) {

View File

@@ -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 = "<group>"; };
7FEC870028A4325D00DE96CB /* NotificationsModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NotificationsModule.m; path = Mattermost/NotificationsModule.m; sourceTree = "<group>"; };
7FEC870428A44A7B00DE96CB /* NotificationsModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NotificationsModule.h; path = Mattermost/NotificationsModule.h; sourceTree = "<group>"; };
7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorSharingView.swift; sourceTree = "<group>"; };
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 = "<group>";
@@ -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 */,

View File

@@ -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")

View File

@@ -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: [])
}
}

View File

@@ -87,12 +87,14 @@ class ShareViewModel: ObservableObject {
func downloadProfileImage(serverUrl: String, userId: String, imageBinding: Binding<UIImage?>) {
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 {

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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")