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>
This commit is contained in:
@@ -509,7 +509,8 @@ func process_metadata_event(image_cache: ImageCache, profiles: Profiles, ev: Nos
|
|||||||
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
||||||
if let url = URL(string: picture) {
|
if let url = URL(string: picture) {
|
||||||
Task<UIImage?, Never>.init(priority: .background) {
|
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 {
|
DispatchQueue.main.async {
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import UIKit
|
||||||
|
|
||||||
enum ImageProcessingStatus {
|
enum ImageProcessingStatus {
|
||||||
case processing
|
case processing
|
||||||
@@ -30,27 +30,27 @@ class ImageCache {
|
|||||||
state[key] = new_state
|
state[key] = new_state
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy var cache: NSCache<AnyObject, UIImage> = {
|
lazy var cache: NSCache<NSString, UIImage> = {
|
||||||
let cache = NSCache<AnyObject, UIImage>()
|
let cache = NSCache<NSString, UIImage>()
|
||||||
cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
|
cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
|
||||||
return cache
|
return cache
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// simple polling until I can figure out a better way to do this
|
// 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 {
|
while true {
|
||||||
let why_would_this_happen: ()? = try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
let why_would_this_happen: ()? = try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||||
if why_would_this_happen == nil {
|
if why_would_this_happen == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if get_state(url.absoluteString) == .done {
|
if get_state(key) == .done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_sync(for url: URL) -> UIImage? {
|
func lookup_sync(key: String) -> UIImage? {
|
||||||
let status = get_state(url.absoluteString)
|
let status = get_state(key)
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case .done:
|
case .done:
|
||||||
@@ -61,61 +61,115 @@ class ImageCache {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let decoded = cache.object(forKey: url as AnyObject) {
|
if let decoded = cache.object(forKey: NSString(string: key)) {
|
||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup(for url: URL) async -> UIImage? {
|
func lookup_or_load_image(key: String, url: URL?) async -> UIImage? {
|
||||||
let status = get_state(url.absoluteString)
|
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 {
|
switch status {
|
||||||
case .done:
|
case .done:
|
||||||
break
|
break
|
||||||
case .processing:
|
case .processing:
|
||||||
await wait_for_image(url)
|
await wait_for_image(key)
|
||||||
case .none:
|
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 decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(for url: URL) {
|
func remove(key: String) {
|
||||||
lock.lock(); defer { lock.unlock() }
|
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 scale = await UIScreen.main.scale
|
||||||
let size = CGSize(width: PFP_SIZE * scale, height: PFP_SIZE * scale)
|
let size = CGSize(width: PFP_SIZE * scale, height: PFP_SIZE * scale)
|
||||||
|
|
||||||
let key = url.absoluteString
|
|
||||||
|
|
||||||
set_state(key, new_state: .processing)
|
set_state(key, new_state: .processing)
|
||||||
|
|
||||||
let decoded_image = await image.byPreparingThumbnail(ofSize: size)
|
let decoded_image = await image.byPreparingThumbnail(ofSize: size)
|
||||||
|
|
||||||
lock.lock()
|
save_to_memory_cache(key: key, img: decoded_image ?? UIImage())
|
||||||
cache.setObject(decoded_image ?? UIImage(), forKey: url as AnyObject)
|
if let img = decoded_image {
|
||||||
state[key] = .done
|
if !save_to_file_cache(key: key, img: img) {
|
||||||
lock.unlock()
|
print("failed saving \(key) pfp to file cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return decoded_image
|
return decoded_image
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
func save_to_file_cache(key: String, img: UIImage, suffix: String = "_pfp") -> Bool {
|
||||||
func load_image(cache: ImageCache, from url: URL) async -> UIImage? {
|
guard let url = get_cache_url(key: key, suffix: suffix) else {
|
||||||
if let image = await cache.lookup(for: url) {
|
return false
|
||||||
return image
|
}
|
||||||
|
|
||||||
|
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(img, forKey: NSString(string: key))
|
||||||
|
state[key] = .done
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func load_image(cache: ImageCache, from url: URL, key: String) async -> UIImage? {
|
||||||
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
|
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -124,5 +178,18 @@ func load_image(cache: ImageCache, from url: URL) async -> UIImage? {
|
|||||||
return nil
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ struct ProfilePicView: View {
|
|||||||
.padding(2)
|
.padding(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProfilePic(_ url: URL) -> some View {
|
var MainContent: some View {
|
||||||
return Group {
|
Group {
|
||||||
if let img = self.img {
|
if let img = self.img {
|
||||||
img
|
img
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -66,27 +66,23 @@ struct ProfilePicView: View {
|
|||||||
Placeholder
|
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 {
|
var body: some View {
|
||||||
MainContent
|
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
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let updated = notif.object as! ProfileUpdate
|
let updated = notif.object as! ProfileUpdate
|
||||||
|
|
||||||
@@ -96,7 +92,8 @@ struct ProfilePicView: View {
|
|||||||
|
|
||||||
if let pic = updated.profile.picture {
|
if let pic = updated.profile.picture {
|
||||||
if let url = URL(string: pic) {
|
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)
|
self.img = Image(uiImage: ui_img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user