damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 355b8c5da8b65a8accd247cac2e76632c3a6c67a
parent 38bfe8d76b399789a7030b03bdb9e24e8625697f
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 16 Oct 2022 11:42:20 -0700

Use kingfisher for profile pic loading

Changelog-Changed: Use an optimized library for image loading
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4----
Mdamus/ContentView.swift | 3++-
Mdamus/Models/DamusState.swift | 3+--
Mdamus/Models/FollowersModel.swift | 2+-
Mdamus/Models/FollowingModel.swift | 2+-
Mdamus/Models/HomeModel.swift | 15+++++----------
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 2+-
Ddamus/Util/ImageCache.swift | 193-------------------------------------------------------------------------------
Mdamus/Views/ChatView.swift | 4++--
Mdamus/Views/DMChatView.swift | 2+-
Mdamus/Views/EventView.swift | 2+-
Mdamus/Views/FollowingView.swift | 2+-
Mdamus/Views/ProfilePicView.swift | 47+++++++++++++++--------------------------------
Mdamus/Views/ProfilePictureSelector.swift | 8+-------
Mdamus/Views/ProfileView.swift | 4++--
Mdamus/Views/ReplyQuoteView.swift | 5++---
17 files changed, 37 insertions(+), 263 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; }; 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; }; 4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; }; - 4C363A8628234FDE006E126D /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8528234FDE006E126D /* ImageCache.swift */; }; 4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; }; 4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; }; 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8B28236B92006E126D /* PubkeyView.swift */; }; @@ -146,7 +145,6 @@ 4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; }; 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; }; 4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; }; - 4C363A8528234FDE006E126D /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; }; 4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; }; 4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; }; 4C363A8B28236B92006E126D /* PubkeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubkeyView.swift; sourceTree = "<group>"; }; @@ -366,7 +364,6 @@ 4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */, 4CE4F8CC281352B30009DFBB /* Notifications.swift */, 4C363A8328233689006E126D /* Parser.swift */, - 4C363A8528234FDE006E126D /* ImageCache.swift */, 4C363AA728297703006E126D /* InsertSort.swift */, 4C477C9D282C3A4800033AA3 /* TipCounter.swift */, 4C285C8B28398BC6008A31F1 /* Keys.swift */, @@ -610,7 +607,6 @@ 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, - 4C363A8628234FDE006E126D /* ImageCache.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -7,6 +7,7 @@ import SwiftUI import Starscream +//import Kingfisher let BOOTSTRAP_RELAYS = [ "wss://relay.damus.io", @@ -204,6 +205,7 @@ struct ContentView: View { } .onAppear() { self.connect() + //KingfisherManager.shared.cache.clearDiskCache() setup_notifications() } .sheet(item: $active_sheet) { item in @@ -371,7 +373,6 @@ struct ContentView: View { boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(), tips: TipCounter(our_pubkey: pubkey), - image_cache: ImageCache(), profiles: Profiles(), dms: home.dms ) diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -14,7 +14,6 @@ struct DamusState { let boosts: EventCounter let contacts: Contacts let tips: TipCounter - let image_cache: ImageCache let profiles: Profiles let dms: DirectMessagesModel @@ -23,6 +22,6 @@ struct DamusState { } static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(), tips: TipCounter(our_pubkey: ""), image_cache: ImageCache(), profiles: Profiles(), dms: DirectMessagesModel()) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel()) } } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift @@ -79,7 +79,7 @@ class FollowersModel: ObservableObject { if ev.known_kind == .contacts { handle_contact_event(ev) } else if ev.known_kind == .metadata { - process_metadata_event(image_cache: damus_state.image_cache, profiles: damus_state.profiles, ev: ev) + process_metadata_event(profiles: damus_state.profiles, ev: ev) } case .notice(let msg): diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift @@ -60,7 +60,7 @@ class FollowingModel { switch nev { case .event(_, let ev): if ev.kind == 0 { - process_metadata_event(image_cache: damus_state.image_cache, profiles: damus_state.profiles, ev: ev) + process_metadata_event(profiles: damus_state.profiles, ev: ev) } case .notice(let msg): print("followingmodel notice: \(msg)") diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -321,7 +321,7 @@ class HomeModel: ObservableObject { } func handle_metadata_event(_ ev: NostrEvent) { - process_metadata_event(image_cache: damus_state.image_cache, profiles: damus_state.profiles, ev: ev) + process_metadata_event(profiles: damus_state.profiles, ev: ev) } func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { @@ -510,7 +510,7 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) { print("-----") } -func process_metadata_event(image_cache: ImageCache, profiles: Profiles, ev: NostrEvent) { +func process_metadata_event(profiles: Profiles, ev: NostrEvent) { guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { return } @@ -527,14 +527,9 @@ func process_metadata_event(image_cache: ImageCache, profiles: Profiles, ev: Nos // load pfps asap let picture = tprof.profile.picture ?? robohash(ev.pubkey) - if let url = URL(string: picture) { - Task<UIImage?, Never>.init(priority: .background) { - 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)) - } - return res + if let _ = URL(string: picture) { + DispatchQueue.main.async { + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) } } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -116,7 +116,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent } if ev.known_kind == .metadata { - process_metadata_event(image_cache: damus_state.image_cache, profiles: damus_state.profiles, ev: ev) + process_metadata_event(profiles: damus_state.profiles, ev: ev) } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -190,7 +190,7 @@ class ThreadModel: ObservableObject { } if ev.known_kind == .metadata { - process_metadata_event(image_cache: damus_state.image_cache, profiles: damus_state.profiles, ev: ev) + process_metadata_event(profiles: damus_state.profiles, ev: ev) } else if ev.is_textlike { self.add_event(ev, privkey: self.damus_state.keypair.privkey) } else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create { diff --git a/damus/Util/ImageCache.swift b/damus/Util/ImageCache.swift @@ -1,193 +0,0 @@ -// -// ImageCache.swift -// damus -// -// Created by William Casarin on 2022-05-04. -// - -import Foundation -import SwiftUI -import UIKit - -enum ImageProcessingStatus { - case processing - case done -} - -class ImageCache { - private let lock = NSLock() - private var state: [String: ImageProcessingStatus] = [:] - - private func get_state(_ key: String) -> ImageProcessingStatus? { - lock.lock(); defer { lock.unlock() } - - return state[key] - } - - private func set_state(_ key: String, new_state: ImageProcessingStatus) { - lock.lock(); defer { lock.unlock() } - - state[key] = new_state - } - - 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(_ 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(key) == .done { - return - } - } - } - - func lookup_sync(key: String) -> UIImage? { - let status = get_state(key) - - switch status { - case .done: - break - case .processing: - return nil - case .none: - return nil - } - - if let decoded = cache.object(forKey: NSString(string: key)) { - return decoded - } - - return nil - } - - 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) - - return img - } - - func lookup(key: String) async -> UIImage? { - let status = get_state(key) - - switch status { - case .done: - break - case .processing: - await wait_for_image(key) - case .none: - return lookup_file_cache(key: key) - } - - if let decoded = cache.object(forKey: NSString(string: key)) { - return decoded - } - - return nil - } - - func remove(key: String) { - lock.lock(); defer { lock.unlock() } - cache.removeObject(forKey: NSString(string: key)) - } - - 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) - - 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(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 { - return nil - } - - guard let img = UIImage(data: data) else { - return nil - } - - 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/ChatView.swift b/damus/Views/ChatView.swift @@ -75,7 +75,7 @@ struct ChatView: View { HStack { VStack { if is_active || just_started { - ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) + ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles) } Spacer() @@ -96,7 +96,7 @@ struct ChatView: View { if let ref_id = thread.replies.lookup(event.id) { if !is_reply_to_prev() { - ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, image_cache: damus_state.image_cache, profiles: damus_state.profiles) + ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, profiles: damus_state.profiles) .frame(maxHeight: expand_reply ? nil : 100) .environmentObject(thread) .onTapGesture { diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -37,7 +37,7 @@ struct DMChatView: View { let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel) return NavigationLink(destination: profile_page) { HStack { - ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) + ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles) ProfileName(pubkey: pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: true) } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -109,7 +109,7 @@ struct EventView: View { let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) NavigationLink(destination: pv) { - ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles) + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles) } Spacer() diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift @@ -18,7 +18,7 @@ struct FollowUserView: View { let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers) NavigationLink(destination: pv) { - ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) + ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) VStack(alignment: .leading) { let profile = damus_state.profiles.lookup(id: target.pubkey) diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Kingfisher let PFP_SIZE: CGFloat = 52.0 @@ -35,11 +36,9 @@ struct ProfilePicView: View { let pubkey: String let size: CGFloat let highlight: Highlight - let image_cache: ImageCache let profiles: Profiles @State var picture: String? = nil - @State var img: Image? = nil var PlaceholderColor: Color { return id_to_color(pubkey) @@ -55,34 +54,24 @@ struct ProfilePicView: View { var MainContent: some View { Group { - if let img = self.img { - img - .resizable() - .frame(width: size, height: size) - .clipShape(Circle()) - .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) - .padding(2) - } else { - Placeholder - } + let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) + let url = URL(string: pic) + let processor = /*DownsamplingImageProcessor(size: CGSize(width: size, height: size)) + |>*/ ResizingImageProcessor(referenceSize: CGSize(width: size, height: size)) + |> RoundCornerImageProcessor(cornerRadius: 20) + KFImage.url(url) + .placeholder { _ in + Placeholder + } + .setProcessor(processor) + .scaleFactor(UIScreen.main.scale) + .loadDiskFileSynchronously() + .fade(duration: 0.1) } } 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 @@ -91,12 +80,7 @@ struct ProfilePicView: View { } if let pic = updated.profile.picture { - if let url = URL(string: pic) { - 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.picture = pic } } } @@ -119,7 +103,6 @@ struct ProfilePicView_Previews: PreviewProvider { pubkey: pubkey, size: 100, highlight: .none, - image_cache: ImageCache(), profiles: make_preview_profiles(pubkey)) } } diff --git a/damus/Views/ProfilePictureSelector.swift b/damus/Views/ProfilePictureSelector.swift @@ -13,13 +13,7 @@ struct ProfilePictureSelector: View { var body: some View { let highlight: Highlight = .custom(Color.white, 2.0) ZStack { - /* - Image(systemName: "camera") - .font(.title) - .foregroundColor(.white) - */ - - ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, image_cache: ImageCache(), profiles: Profiles()) + ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles()) } } } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -93,7 +93,7 @@ struct ProfileView: View { let data = damus_state.profiles.lookup(id: profile.pubkey) HStack(alignment: .center) { - ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE, highlight: .custom(Color.black, 2), image_cache: damus_state.image_cache, profiles: damus_state.profiles) + ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE, highlight: .custom(Color.black, 2), profiles: damus_state.profiles) ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts) @@ -181,7 +181,7 @@ struct ProfileView_Previews: PreviewProvider { func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" - let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(), tips: TipCounter(our_pubkey: pubkey), image_cache: ImageCache(), profiles: Profiles(), dms: DirectMessagesModel()) + let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel()) let prof = Profile(name: "damus", display_name: "Damus", about: "iOS app!", picture: "https://damus.io/img/logo.png") let tsprof = TimestampedProfile(profile: prof, timestamp: 0) diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift @@ -11,7 +11,6 @@ struct ReplyQuoteView: View { let privkey: String? let quoter: NostrEvent let event_id: String - let image_cache: ImageCache let profiles: Profiles @EnvironmentObject var thread: ThreadModel @@ -25,7 +24,7 @@ struct ReplyQuoteView: View { VStack(alignment: .leading) { HStack(alignment: .top) { - ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, image_cache: image_cache, profiles: profiles) + ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, profiles: profiles) Text(Profile.displayName(profile: profiles.lookup(id: event.pubkey), pubkey: event.pubkey)) .foregroundColor(.accentColor) Text("\(format_relative_time(event.created_at))") @@ -59,7 +58,7 @@ struct ReplyQuoteView_Previews: PreviewProvider { static var previews: some View { let s = test_damus_state() let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey") - ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", image_cache: s.image_cache, profiles: s.profiles) + ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", profiles: s.profiles) .environmentObject(ThreadModel(event: quoter, damus_state: s)) } }