commit 366e3565d17b4c41e69b23f970d2d1e3129895fd
parent 7da7bcdfd5aa490e4c913f2d0568ed53f901c967
Author: William Casarin <jb55@jb55.com>
Date: Sat, 6 Aug 2022 19:22:54 -0700
pfp: profile pic image cache
So we don't have to download 60MB of profile pics every time we load the
app..
Changelog-Added: Added profile picture cache
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
3 files changed, 113 insertions(+), 48 deletions(-)
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
@@ -509,7 +509,8 @@ func process_metadata_event(image_cache: ImageCache, profiles: Profiles, ev: Nos
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if let url = URL(string: picture) {
Task<UIImage?, Never>.init(priority: .background) {
- let res = await load_image(cache: image_cache, from: url)
+ let pfp_key = pfp_cache_key(url: url)
+ let res = await image_cache.lookup_or_load_image(key: pfp_key, url: url)
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
diff --git a/damus/Util/ImageCache.swift b/damus/Util/ImageCache.swift
@@ -7,7 +7,7 @@
import Foundation
import SwiftUI
-import Combine
+import UIKit
enum ImageProcessingStatus {
case processing
@@ -30,27 +30,27 @@ class ImageCache {
state[key] = new_state
}
- lazy var cache: NSCache<AnyObject, UIImage> = {
- let cache = NSCache<AnyObject, UIImage>()
+ lazy var cache: NSCache<NSString, UIImage> = {
+ let cache = NSCache<NSString, UIImage>()
cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
return cache
}()
// simple polling until I can figure out a better way to do this
- func wait_for_image(_ url: URL) async {
+ func wait_for_image(_ key: String) async {
while true {
let why_would_this_happen: ()? = try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
if why_would_this_happen == nil {
return
}
- if get_state(url.absoluteString) == .done {
+ if get_state(key) == .done {
return
}
}
}
- func lookup_sync(for url: URL) -> UIImage? {
- let status = get_state(url.absoluteString)
+ func lookup_sync(key: String) -> UIImage? {
+ let status = get_state(key)
switch status {
case .done:
@@ -61,61 +61,115 @@ class ImageCache {
return nil
}
- if let decoded = cache.object(forKey: url as AnyObject) {
+ if let decoded = cache.object(forKey: NSString(string: key)) {
return decoded
}
return nil
}
- func lookup(for url: URL) async -> UIImage? {
- let status = get_state(url.absoluteString)
+ func lookup_or_load_image(key: String, url: URL?) async -> UIImage? {
+ if let img = await lookup(key: key) {
+ return img
+ }
+
+ guard let url = url else {
+ return nil
+ }
+
+ return await load_image(cache: self, from: url, key: key)
+ }
+
+ func get_cache_url(key: String, suffix: String, ext: String = "png") -> URL? {
+ let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
+
+ guard let root = urls.first else {
+ return nil
+ }
+
+ return root.appendingPathComponent("\(key)\(suffix).\(ext)")
+ }
+
+ private func lookup_file_cache(key: String, suffix: String = "_pfp") -> UIImage? {
+ guard let img_file = get_cache_url(key: key, suffix: suffix) else {
+ return nil
+ }
+
+ guard let img = UIImage(contentsOfFile: img_file.path) else {
+ //print("failed to load \(key)\(suffix).png from file cache")
+ return nil
+ }
+
+ save_to_memory_cache(key: key, img: img)
+
+ print("loaded \(key)\(suffix).png from file cache")
+
+ return img
+ }
+
+ func lookup(key: String) async -> UIImage? {
+ let status = get_state(key)
switch status {
case .done:
break
case .processing:
- await wait_for_image(url)
+ await wait_for_image(key)
case .none:
- return nil
+ return lookup_file_cache(key: key)
}
- if let decoded = cache.object(forKey: url as AnyObject) {
+ if let decoded = cache.object(forKey: NSString(string: key)) {
return decoded
}
return nil
}
- func remove(for url: URL) {
+ func remove(key: String) {
lock.lock(); defer { lock.unlock() }
- cache.removeObject(forKey: url as AnyObject)
+ cache.removeObject(forKey: NSString(string: key))
}
- func insert(_ image: UIImage, for url: URL) async -> UIImage? {
+ func insert(_ image: UIImage, key: String) async -> UIImage? {
let scale = await UIScreen.main.scale
let size = CGSize(width: PFP_SIZE * scale, height: PFP_SIZE * scale)
- let key = url.absoluteString
-
set_state(key, new_state: .processing)
let decoded_image = await image.byPreparingThumbnail(ofSize: size)
+ save_to_memory_cache(key: key, img: decoded_image ?? UIImage())
+ if let img = decoded_image {
+ if !save_to_file_cache(key: key, img: img) {
+ print("failed saving \(key) pfp to file cache")
+ }
+ }
+
+ return decoded_image
+ }
+
+ func save_to_file_cache(key: String, img: UIImage, suffix: String = "_pfp") -> Bool {
+ guard let url = get_cache_url(key: key, suffix: suffix) else {
+ return false
+ }
+
+ guard let data = img.pngData() else {
+ return false
+ }
+
+ return (try? data.write(to: url)) != nil
+ }
+
+ func save_to_memory_cache(key: String, img: UIImage) {
lock.lock()
- cache.setObject(decoded_image ?? UIImage(), forKey: url as AnyObject)
+ cache.setObject(img, forKey: NSString(string: key))
state[key] = .done
lock.unlock()
-
- return decoded_image
}
}
-func load_image(cache: ImageCache, from url: URL) async -> UIImage? {
- if let image = await cache.lookup(for: url) {
- return image
- }
-
+func load_image(cache: ImageCache, from url: URL, key: String) async -> UIImage? {
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
return nil
}
@@ -124,5 +178,18 @@ func load_image(cache: ImageCache, from url: URL) async -> UIImage? {
return nil
}
- return await cache.insert(img, for: url)
+ return await cache.insert(img, key: key)
+}
+
+
+func hashed_hexstring(_ str: String) -> String {
+ guard let data = str.data(using: .utf8) else {
+ return str
+ }
+
+ return hex_encode(sha256(data))
+}
+
+func pfp_cache_key(url: URL) -> String {
+ return hashed_hexstring(url.absoluteString)
}
diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift
@@ -53,8 +53,8 @@ struct ProfilePicView: View {
.padding(2)
}
- func ProfilePic(_ url: URL) -> some View {
- return Group {
+ var MainContent: some View {
+ Group {
if let img = self.img {
img
.resizable()
@@ -66,27 +66,23 @@ struct ProfilePicView: View {
Placeholder
}
}
- .task {
- let ui_img = await load_image(cache: image_cache, from: url)
- if let ui_img = ui_img {
- self.img = Image(uiImage: ui_img)
- }
- }
- }
-
- var MainContent: some View {
- Group {
- let picture = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
- if let pic_url = URL(string: picture) {
- ProfilePic(pic_url)
- } else {
- Placeholder
- }
- }
}
var body: some View {
MainContent
+ .task {
+ let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
+ guard let url = URL(string: pic) else {
+ return
+ }
+ let pfp_key = pfp_cache_key(url: url)
+ let ui_img = await image_cache.lookup_or_load_image(key: pfp_key, url: url)
+
+ if let ui_img = ui_img {
+ self.img = Image(uiImage: ui_img)
+ return
+ }
+ }
.onReceive(handle_notify(.profile_updated)) { notif in
let updated = notif.object as! ProfileUpdate
@@ -96,7 +92,8 @@ struct ProfilePicView: View {
if let pic = updated.profile.picture {
if let url = URL(string: pic) {
- if let ui_img = image_cache.lookup_sync(for: url) {
+ let pfp_key = pfp_cache_key(url: url)
+ if let ui_img = image_cache.lookup_sync(key: pfp_key) {
self.img = Image(uiImage: ui_img)
}
}