forked from Ivasoft/mattermost-mobile
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:
@@ -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>
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
54
ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift
Normal file
54
ios/MattermostShare/Views/ErrorViews/ErrorSharingView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user