forked from Ivasoft/mattermost-mobile
Cache push notification profile image and add logs (#6932)
* Cache push notification profile image and add logs * Fix indent
This commit is contained in:
@@ -65,7 +65,7 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await deleteFileCache(serverUrl);
|
||||
await getAllCachedFiles();
|
||||
getAllCachedFiles();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -166,32 +166,41 @@ export async function deleteV1Data() {
|
||||
|
||||
export async function deleteFileCache(serverUrl: string) {
|
||||
const serverDir = urlSafeBase64Encode(serverUrl);
|
||||
deleteFileCacheByDir(serverDir);
|
||||
return deleteFileCacheByDir(serverDir);
|
||||
}
|
||||
|
||||
export async function deleteLegacyFileCache(serverUrl: string) {
|
||||
const serverDir = hashCode_DEPRECATED(serverUrl);
|
||||
deleteFileCacheByDir(serverDir);
|
||||
return deleteFileCacheByDir(serverDir);
|
||||
}
|
||||
|
||||
export async function deleteFileCacheByDir(dir: string) {
|
||||
if (Platform.OS === 'ios') {
|
||||
const appGroupCacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${dir}`;
|
||||
await deleteFilesInDir(appGroupCacheDir);
|
||||
}
|
||||
|
||||
const cacheDir = `${FileSystem.CachesDirectoryPath}/${dir}`;
|
||||
if (cacheDir) {
|
||||
const cacheDirInfo = await FileSystem.exists(cacheDir);
|
||||
await deleteFilesInDir(cacheDir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function deleteFilesInDir(directory: string) {
|
||||
if (directory) {
|
||||
const cacheDirInfo = await FileSystem.exists(directory);
|
||||
if (cacheDirInfo) {
|
||||
if (Platform.OS === 'ios') {
|
||||
await FileSystem.unlink(cacheDir);
|
||||
await FileSystem.mkdir(cacheDir);
|
||||
await FileSystem.unlink(directory);
|
||||
await FileSystem.mkdir(directory);
|
||||
} else {
|
||||
const lstat = await FileSystem.readDir(cacheDir);
|
||||
const lstat = await FileSystem.readDir(directory);
|
||||
lstat.forEach((stat: FileSystem.ReadDirItem) => {
|
||||
FileSystem.unlink(stat.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function lookupMimeType(filename: string) {
|
||||
@@ -525,8 +534,18 @@ export const getAllFilesInCachesDirectory = async (serverUrl: string) => {
|
||||
try {
|
||||
const files: FileSystem.ReadDirItem[][] = [];
|
||||
|
||||
const directoryFiles = await FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${urlSafeBase64Encode(serverUrl)}`);
|
||||
files.push(directoryFiles);
|
||||
const promises = [FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${urlSafeBase64Encode(serverUrl)}`)];
|
||||
if (Platform.OS === 'ios') {
|
||||
const cacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${urlSafeBase64Encode(serverUrl)}`;
|
||||
promises.push(FileSystem.readDir(cacheDir));
|
||||
}
|
||||
|
||||
const dirs = await Promise.allSettled(promises);
|
||||
dirs.forEach((p) => {
|
||||
if (p.status === 'fulfilled') {
|
||||
files.push(p.value);
|
||||
}
|
||||
});
|
||||
|
||||
const flattenedFiles = files.flat();
|
||||
const totalSize = flattenedFiles.reduce((acc, file) => acc + file.size, 0);
|
||||
|
||||
64
ios/Gekidou/Sources/Gekidou/FileCache.swift
Normal file
64
ios/Gekidou/Sources/Gekidou/FileCache.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// FileCache.swift
|
||||
// Gekidou
|
||||
//
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public class FileCache: NSObject {
|
||||
private var cacheURL: URL?
|
||||
@objc public static let `default` = FileCache()
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
let filemgr = FileManager.default
|
||||
let appGroupId = Bundle.main.infoDictionary!["AppGroupIdentifier"] as! String
|
||||
let containerUrl = filemgr.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
|
||||
if let url = containerUrl,
|
||||
let cacheURL = url.appendingPathComponent("Library", isDirectory: true) as URL? {
|
||||
self.cacheURL = cacheURL.appendingPathComponent("Caches", isDirectory: true)
|
||||
self.createDirectoryIfNeeded(directory: self.cacheURL)
|
||||
}
|
||||
}
|
||||
|
||||
private func createDirectoryIfNeeded(directory: URL?) {
|
||||
var isDirectory = ObjCBool(false)
|
||||
|
||||
if let cachePath = directory?.path {
|
||||
let exists = FileManager.default.fileExists(atPath: cachePath, isDirectory: &isDirectory)
|
||||
|
||||
if !exists && !isDirectory.boolValue {
|
||||
try? FileManager.default.createDirectory(atPath: cachePath, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getUrlImageFor(serverUrl: String, userId: String) -> URL? {
|
||||
guard let url = cacheURL else {return nil}
|
||||
|
||||
let serverCacheURL = url.appendingPathComponent(serverUrl.toUrlSafeBase64Encode(), isDirectory: true)
|
||||
createDirectoryIfNeeded(directory: serverCacheURL)
|
||||
return serverCacheURL.appendingPathComponent(userId + ".png")
|
||||
}
|
||||
|
||||
public func getProfileImage(serverUrl: String, userId: String) -> UIImage? {
|
||||
guard let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) else { return nil }
|
||||
return UIImage(contentsOfFile: url.path)
|
||||
}
|
||||
|
||||
public func saveProfileImage(serverUrl: String, userId: String, imageData: Data?) {
|
||||
guard let data = imageData,
|
||||
let url = getUrlImageFor(serverUrl: serverUrl, userId: userId)
|
||||
else { return }
|
||||
|
||||
do {
|
||||
try data.write(to: url)
|
||||
} catch let error {
|
||||
print("Erro saving image. \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import SQLite
|
||||
import os.log
|
||||
|
||||
public struct AckNotification: Codable {
|
||||
let type: String
|
||||
@@ -66,6 +67,14 @@ extension String {
|
||||
guard self.hasPrefix(prefix) else { return self }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
|
||||
func toUrlSafeBase64Encode() -> String {
|
||||
return Data(
|
||||
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
|
||||
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
|
||||
.utf8
|
||||
).base64EncodedString()
|
||||
}
|
||||
}
|
||||
|
||||
extension Network {
|
||||
@@ -93,8 +102,18 @@ extension Network {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 && error == nil {
|
||||
imgData = data
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if (httpResponse.statusCode == 200 && error == nil) {
|
||||
imgData = data
|
||||
FileCache.default.saveProfileImage(serverUrl: serverUrl, userId: senderId, imageData: data)
|
||||
} else {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
|
||||
httpResponse.statusCode,
|
||||
(error?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
@@ -103,9 +122,14 @@ extension Network {
|
||||
let url = URL(string: overrideUrl) {
|
||||
request(url, withMethod: "GET", withServerUrl: "", completionHandler: processResponse)
|
||||
} else {
|
||||
let endpoint = "/users/\(senderId)/image"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: processResponse)
|
||||
if let image = FileCache.default.getProfileImage(serverUrl: serverUrl, userId: senderId) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: cached image")
|
||||
imgData = image.pngData()
|
||||
semaphore.signal()
|
||||
} else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
|
||||
fetchUserProfilePicture(userId: senderId, withServerUrl: serverUrl, completionHandler: processResponse)
|
||||
}
|
||||
}
|
||||
|
||||
semaphore.wait()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Gekidou
|
||||
import UserNotifications
|
||||
import Intents
|
||||
import os.log
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
let preferences = Gekidou.Preferences.default
|
||||
@@ -24,13 +25,24 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
|
||||
fetchReceipt(ackNotification)
|
||||
} else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
|
||||
sendMessageIntent(notification: request.content)
|
||||
}
|
||||
}
|
||||
|
||||
func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent) {
|
||||
bestAttemptContent.userInfo["server_url"] = serverUrl
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
|
||||
serverUrl
|
||||
)
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any]
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: parsed json response %{public}@",
|
||||
String(describing: json != nil)
|
||||
)
|
||||
if let json = json {
|
||||
if let message = json["message"] as? String {
|
||||
bestAttemptContent.body = message
|
||||
@@ -49,10 +61,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
if (preferences.object(forKey: "ApplicationIsForeground") as? String != "true") {
|
||||
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {[weak self] notification in
|
||||
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent(notification: bestAttemptContent)
|
||||
})
|
||||
} else {
|
||||
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
|
||||
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
|
||||
sendMessageIntent(notification: bestAttemptContent)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +74,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: service extension time expired")
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
|
||||
sendMessageIntent(notification: bestAttemptContent)
|
||||
}
|
||||
}
|
||||
@@ -71,68 +87,78 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let channelId = notification.userInfo["channel_id"] as! String
|
||||
let rootId = notification.userInfo.index(forKey: "root_id") != nil ? notification.userInfo["root_id"] as! String : ""
|
||||
let senderId = notification.userInfo["sender_id"] as? String ?? ""
|
||||
let senderName = notification.userInfo["sender_name"] 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 avatarData = Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl)
|
||||
|
||||
let handle = INPersonHandle(value: notification.userInfo["sender_id"] as? String, type: .unknown)
|
||||
var avatar: INImage?
|
||||
if let imgData = avatarData {
|
||||
avatar = INImage(imageData: imgData)
|
||||
}
|
||||
|
||||
let sender = INPerson(personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: channelName,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil)
|
||||
var conversationId = channelId
|
||||
if isCRTEnabled && rootId != "" {
|
||||
conversationId = rootId
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let updatedContent = try notification.updating(from: intent)
|
||||
self.contentHandler?(updatedContent)
|
||||
} catch {
|
||||
self.contentHandler?(notification)
|
||||
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
|
||||
}
|
||||
|
||||
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 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 fetchReceipt(_ ackNotification: AckNotification) -> Void {
|
||||
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
|
||||
sendMessageIntent(notification: bestAttemptContent!)
|
||||
return
|
||||
}
|
||||
|
||||
Network.default.postNotificationReceipt(ackNotification) {data, response, error in
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent",
|
||||
httpResponse.statusCode
|
||||
)
|
||||
self.sendMessageIntent(notification: self.bestAttemptContent!)
|
||||
return
|
||||
}
|
||||
@@ -143,6 +169,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
|
||||
self.retryIndex
|
||||
)
|
||||
self.fetchReceipt(ackNotification)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user