forked from Ivasoft/mattermost-mobile
Use LRU to cache the Avatar shown in push notifications (#7124)
* iOS switch from file cache to memory cache and use last_picture_update to update the avatar if needed * Android switch from file cache to memory cache and use last_picture_update to update the avatar if needed, split function to multiple files and catch potential exceptions
This commit is contained in:
13
ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift
Normal file
13
ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
extension ImageCache {
|
||||
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
let key = "\(serverUrl)-\(userId)-\(updatedAt)" as NSString
|
||||
if let image = imageCache.object(forKey: key) as? Data {
|
||||
return image
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
extension ImageCache {
|
||||
public func removeAllImages() {
|
||||
imageCache.removeAllObjects()
|
||||
keysCache.removeAllObjects()
|
||||
}
|
||||
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String ) {
|
||||
guard let data = data else {
|
||||
return removeImage(for: userId, onServer: serverUrl)
|
||||
}
|
||||
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
let cacheKey = "\(serverUrl)-\(userId)" as NSString
|
||||
let imageKey = "\(cacheKey)-\(updatedAt)" as NSString
|
||||
imageCache.setObject(NSData(data: data), forKey: imageKey as NSString, cost: data.count)
|
||||
keysCache.setObject(imageKey, forKey: cacheKey)
|
||||
}
|
||||
|
||||
func removeImage(for userId: String, onServer serverUrl: String) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
let cacheKey = "\(serverUrl)-\(userId)" as NSString
|
||||
if let key = keysCache.object(forKey: cacheKey) {
|
||||
keysCache.removeObject(forKey: cacheKey)
|
||||
imageCache.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ios/Gekidou/Sources/Gekidou/Cache/ImageCache.swift
Normal file
31
ios/Gekidou/Sources/Gekidou/Cache/ImageCache.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
public final class ImageCache: ImageCacheType {
|
||||
public static let `default` = ImageCache()
|
||||
struct Config {
|
||||
let countLimit: Int
|
||||
let memoryLimit: Int
|
||||
|
||||
static let defaultConfig = Config(countLimit: 50, memoryLimit: 1024 * 1024 * 50)
|
||||
}
|
||||
|
||||
lazy var imageCache: NSCache<NSString, NSData> = {
|
||||
let cache = NSCache<NSString, NSData>()
|
||||
cache.countLimit = config.countLimit
|
||||
cache.totalCostLimit = config.memoryLimit
|
||||
return cache
|
||||
}()
|
||||
|
||||
lazy var keysCache: NSCache<NSString, NSString> = {
|
||||
let cache = NSCache<NSString, NSString>()
|
||||
cache.countLimit = config.countLimit
|
||||
return cache
|
||||
}()
|
||||
|
||||
let lock = NSLock()
|
||||
let config: Config
|
||||
|
||||
private init(config: Config = Config.defaultConfig) {
|
||||
self.config = config
|
||||
}
|
||||
}
|
||||
11
ios/Gekidou/Sources/Gekidou/Cache/ImageCacheType.swift
Normal file
11
ios/Gekidou/Sources/Gekidou/Cache/ImageCacheType.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
protocol ImageCacheType: class {
|
||||
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data?
|
||||
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String )
|
||||
|
||||
func removeImage(for userId: String, onServer serverUrl: String)
|
||||
|
||||
func removeAllImages()
|
||||
}
|
||||
@@ -24,8 +24,8 @@ extension Network {
|
||||
return request(url, withMethod: "POST", withBody: data, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchUserProfilePicture(userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users/\(userId)/image"
|
||||
public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users/\(userId)/image?lastPictureUpdate=\(lastUpdateAt)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
|
||||
@@ -98,10 +98,11 @@ extension Network {
|
||||
}
|
||||
|
||||
public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) {
|
||||
var updatedAt: Double = 0
|
||||
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if (httpResponse.statusCode == 200 && error == nil) {
|
||||
FileCache.default.saveProfileImage(serverUrl: serverUrl, userId: senderId, imageData: data)
|
||||
ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, onServer: serverUrl)
|
||||
completionHandler(data)
|
||||
} else {
|
||||
os_log(
|
||||
@@ -118,12 +119,16 @@ extension Network {
|
||||
let url = URL(string: overrideUrl) {
|
||||
request(url, withMethod: "GET", withServerUrl: "", completionHandler: processResponse)
|
||||
} else {
|
||||
if let image = FileCache.default.getProfileImage(serverUrl: serverUrl, userId: senderId) {
|
||||
if let lastUpdateAt = Database.default.getUserLastPictureAt(for: senderId, withServerUrl: serverUrl) {
|
||||
updatedAt = lastUpdateAt
|
||||
}
|
||||
if let image = ImageCache.default.image(for: senderId, updatedAt: updatedAt, onServer: serverUrl) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: cached image")
|
||||
completionHandler(image.pngData())
|
||||
completionHandler(image)
|
||||
} else {
|
||||
ImageCache.default.removeImage(for: senderId, onServer: serverUrl)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
|
||||
fetchUserProfilePicture(userId: senderId, withServerUrl: serverUrl, completionHandler: processResponse)
|
||||
fetchUserProfilePicture(userId: senderId, lastUpdateAt: updatedAt, withServerUrl: serverUrl, completionHandler: processResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ import SQLite
|
||||
public struct User: Codable, Hashable {
|
||||
let id: String
|
||||
let auth_service: String
|
||||
let update_at: Int64
|
||||
let delete_at: Int64
|
||||
let update_at: Double
|
||||
let delete_at: Double
|
||||
let email: String
|
||||
let first_name: String
|
||||
let is_bot: Bool
|
||||
let is_guest: Bool
|
||||
let last_name: String
|
||||
let last_picture_update: Int64
|
||||
let last_picture_update: Double
|
||||
let locale: String
|
||||
let nickname: String
|
||||
let position: String
|
||||
@@ -54,36 +54,36 @@ public struct User: Codable, Hashable {
|
||||
let container = try decoder.container(keyedBy: UserKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
auth_service = try container.decode(String.self, forKey: .auth_service)
|
||||
update_at = try container.decode(Int64.self, forKey: .update_at)
|
||||
delete_at = try container.decode(Int64.self, forKey: .delete_at)
|
||||
update_at = (try? container.decodeIfPresent(Double.self, forKey: .update_at)) ?? 0
|
||||
delete_at = (try? container.decodeIfPresent(Double.self, forKey: .delete_at)) ?? 0
|
||||
email = try container.decode(String.self, forKey: .email)
|
||||
first_name = try container.decode(String.self, forKey: .first_name)
|
||||
is_bot = container.contains(.is_bot) ? try container.decode(Bool.self, forKey: .is_bot) : false
|
||||
roles = try container.decode(String.self, forKey: .roles)
|
||||
is_guest = roles.contains("system_guest")
|
||||
last_name = try container.decode(String.self, forKey: .last_name)
|
||||
last_picture_update = try container.decodeIfPresent(Int64.self, forKey: .last_picture_update) ?? 0
|
||||
last_picture_update = (try? container.decodeIfPresent(Double.self, forKey: .last_picture_update)) ?? 0
|
||||
locale = try container.decode(String.self, forKey: .locale)
|
||||
nickname = try container.decode(String.self, forKey: .nickname)
|
||||
position = try container.decode(String.self, forKey: .position)
|
||||
status = "offline"
|
||||
username = try container.decode(String.self, forKey: .username)
|
||||
|
||||
let notifyPropsData = try container.decodeIfPresent([String: String].self, forKey: .notify_props)
|
||||
let notifyPropsData = try? container.decodeIfPresent([String: String].self, forKey: .notify_props)
|
||||
if (notifyPropsData != nil) {
|
||||
notify_props = Database.default.json(from: notifyPropsData) ?? "{}"
|
||||
} else {
|
||||
notify_props = "{}"
|
||||
}
|
||||
|
||||
let propsData = try container.decodeIfPresent([String: String].self, forKey: .props)
|
||||
let propsData = try? container.decodeIfPresent([String: String].self, forKey: .props)
|
||||
if (propsData != nil) {
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
} else {
|
||||
props = "{}"
|
||||
}
|
||||
|
||||
let timezoneData = try container.decodeIfPresent([String: String].self, forKey: .timezone)
|
||||
let timezoneData = try? container.decodeIfPresent([String: String].self, forKey: .timezone)
|
||||
if (timezoneData != nil) {
|
||||
timezone = Database.default.json(from: timezoneData) ?? "{}"
|
||||
} else {
|
||||
@@ -119,6 +119,24 @@ extension Database {
|
||||
|
||||
throw DatabaseError.NoResults(query.expression.description)
|
||||
}
|
||||
|
||||
public func getUserLastPictureAt(for userId: String, withServerUrl serverUrl: String) -> Double? {
|
||||
let idCol = Expression<String>("id")
|
||||
var updateAt: Double?
|
||||
do {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let stmtString = "SELECT * FROM User WHERE id='\(userId)'"
|
||||
|
||||
let results: [User] = try db.prepareRowIterator(stmtString).map {try $0.decode()}
|
||||
updateAt = results.first?.last_picture_update
|
||||
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
return updateAt
|
||||
}
|
||||
|
||||
public func queryUsers(byIds: Set<String>, withServerUrl: String) throws -> Set<String> {
|
||||
let db = try getDatabaseForServer(withServerUrl)
|
||||
@@ -180,14 +198,14 @@ extension Database {
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- user.id)
|
||||
setter.append(authService <- user.auth_service)
|
||||
setter.append(updateAt <- user.update_at)
|
||||
setter.append(deleteAt <- user.delete_at)
|
||||
setter.append(updateAt <- Int64(user.update_at))
|
||||
setter.append(deleteAt <- Int64(user.delete_at))
|
||||
setter.append(email <- user.email)
|
||||
setter.append(firstName <- user.first_name)
|
||||
setter.append(isBot <- user.is_bot)
|
||||
setter.append(isGuest <- user.is_guest)
|
||||
setter.append(lastName <- user.last_name)
|
||||
setter.append(lastPictureUpdate <- user.last_picture_update)
|
||||
setter.append(lastPictureUpdate <- Int64(user.last_picture_update))
|
||||
setter.append(locale <- user.locale)
|
||||
setter.append(nickname <- user.nickname)
|
||||
setter.append(position <- user.position)
|
||||
|
||||
@@ -91,7 +91,7 @@ class ShareViewModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, withServerUrl: serverUrl, completionHandler: {data, response, error in
|
||||
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, withServerUrl: serverUrl, completionHandler: {data, response, error in
|
||||
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
|
||||
debugPrint("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))")
|
||||
return
|
||||
|
||||
@@ -89,9 +89,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
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 message = (notification.userInfo["message"] as? String ?? "")
|
||||
let overrideUsername = notification.userInfo["override_username"] as? String
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let senderIdentifier = overrideUsername ?? senderId
|
||||
let avatar = INImage(imageData: imgData) as INImage?
|
||||
|
||||
var conversationId = channelId
|
||||
@@ -99,7 +101,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
conversationId = rootId
|
||||
}
|
||||
|
||||
let handle = INPersonHandle(value: senderId, type: .unknown)
|
||||
let handle = INPersonHandle(value: senderIdentifier, type: .unknown)
|
||||
let sender = INPerson(personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: channelName,
|
||||
@@ -140,18 +142,22 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
func sendMessageIntent(notification: UNNotificationContent) {
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
let overrideUsername = notification.userInfo["override_username"] as? String
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let sender = overrideUsername ?? senderId
|
||||
|
||||
guard let serverUrl = notification.userInfo["server_url"] as? String,
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let sender = sender
|
||||
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
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender)
|
||||
|
||||
Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl) {[weak self] data in
|
||||
Network.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in
|
||||
self?.sendMessageIntentCompletion(notification, data)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user