commit 4704431c7449080c949c0048c3d560c185ba092d
parent 96d8d854e20f15001f490bc233f62c817517941d
Author: William Casarin <jb55@jb55.com>
Date: Wed, 4 May 2022 18:49:40 -0700
image cache
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
12 files changed, 141 insertions(+), 26 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -14,6 +14,7 @@
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
+ 4C363A8628234FDE006E126D /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8528234FDE006E126D /* ImageCache.swift */; };
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */; };
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */; };
@@ -81,6 +82,7 @@
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.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>"; };
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModel.swift; sourceTree = "<group>"; };
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrKind.swift; sourceTree = "<group>"; };
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.swift; sourceTree = "<group>"; };
@@ -213,6 +215,7 @@
isa = PBXGroup;
children = (
4C363A8328233689006E126D /* Parser.swift */,
+ 4C363A8528234FDE006E126D /* ImageCache.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -428,6 +431,7 @@
buildActionMask = 2147483647;
files = (
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
+ 4C363A8628234FDE006E126D /* ImageCache.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -303,7 +303,9 @@ struct ContentView: View {
self.damus = DamusState(pool: pool, pubkey: pubkey,
likes: EventCounter(our_pubkey: pubkey),
- boosts: EventCounter(our_pubkey: pubkey))
+ boosts: EventCounter(our_pubkey: pubkey),
+ image_cache: ImageCache()
+ )
pool.connect()
}
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -12,5 +12,6 @@ struct DamusState {
let pubkey: String
let likes: EventCounter
let boosts: EventCounter
+ let image_cache: ImageCache
}
diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift
@@ -6,7 +6,65 @@
//
import Foundation
+import UIKit
+import Combine
+class ImageCache {
+ private let lock = NSLock()
+
+ lazy var cache: NSCache<AnyObject, AnyObject> = {
+ let cache = NSCache<AnyObject, AnyObject>()
+ cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
+ return cache
+ }()
+
+ func lookup(for url: URL) -> UIImage? {
+ lock.lock(); defer { lock.unlock() }
+
+ if let decoded = cache.object(forKey: url as AnyObject) as? UIImage {
+ return decoded
+ }
+
+ return nil
+ }
+
+ func remove(for url: URL) {
+ lock.lock(); defer { lock.unlock() }
+ cache.removeObject(forKey: url as AnyObject)
+ }
+
+ func insert(_ image: UIImage?, for url: URL) {
+ guard let image = image else { return remove(for: url) }
+ let decodedImage = image.decodedImage(Int(PFP_SIZE!))
+ lock.lock(); defer { lock.unlock() }
+ cache.setObject(decodedImage, forKey: url as AnyObject)
+ }
+
+ subscript(_ key: URL) -> UIImage? {
+ get {
+ return lookup(for: key)
+ }
+ set {
+ return insert(newValue, for: key)
+ }
+ }
+}
+
+func load_image(cache: ImageCache, from url: URL) -> AnyPublisher<UIImage?, Never> {
+ if let image = cache[url] {
+ return Just(image).eraseToAnyPublisher()
+ }
+ return URLSession.shared.dataTaskPublisher(for: url)
+ .map { (data, response) -> UIImage? in return UIImage(data: data) }
+ .catch { error in return Just(nil) }
+ .handleEvents(receiveOutput: { image in
+ guard let image = image else { return }
+ cache[url] = image
+ })
+ .subscribe(on: DispatchQueue.global(qos: .background))
+ .receive(on: RunLoop.main)
+ .eraseToAnyPublisher()
+}
class Profiles: ObservableObject {
@Published var profiles: [String: TimestampedProfile] = [:]
diff --git a/damus/Util/ImageCache.swift b/damus/Util/ImageCache.swift
@@ -0,0 +1,28 @@
+//
+// ImageCache.swift
+// damus
+//
+// Created by William Casarin on 2022-05-04.
+//
+
+import Foundation
+import SwiftUI
+
+extension UIImage {
+ func decodedImage(_ size: Int) -> UIImage {
+ guard let cgImage = cgImage else { return self }
+ let scale = UIScreen.main.scale
+ let pix_size = CGFloat(size) * scale
+ let colorSpace = CGColorSpaceCreateDeviceRGB()
+ //let cgsize = CGSize(width: size, height: size)
+
+ let context = CGContext(data: nil, width: Int(pix_size), height: Int(pix_size), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
+
+ //UIGraphicsBeginImageContextWithOptions(cgsize, true, 0)
+ context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: pix_size, height: pix_size))
+ //UIGraphicsEndImageContext()
+
+ guard let decodedImage = context?.makeImage() else { return self }
+ return UIImage(cgImage: decodedImage, scale: scale, orientation: .up)
+ }
+}
diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift
@@ -12,8 +12,7 @@ struct ChatView: View {
let prev_ev: NostrEvent?
let next_ev: NostrEvent?
- let likes: EventCounter
- let our_pubkey: String
+ let damus: DamusState
@EnvironmentObject var profiles: Profiles
@EnvironmentObject var thread: ThreadModel
@@ -91,7 +90,7 @@ struct ChatView: View {
VStack {
if is_active || just_started {
- ProfilePicView(picture: profile?.picture, size: 32, highlight: is_active ? .main : .none)
+ ProfilePicView(picture: profile?.picture, size: 32, highlight: is_active ? .main : .none, image_cache: damus.image_cache)
}
/*
if just_started {
@@ -122,7 +121,7 @@ struct ChatView: View {
if let ref_id = thread.replies.lookup(event.id) {
if !is_reply_to_prev() {
- ReplyQuoteView(quoter: event, event_id: ref_id)
+ ReplyQuoteView(quoter: event, event_id: ref_id, image_cache: damus.image_cache)
.environmentObject(thread)
.environmentObject(profiles)
ReplyDescription
@@ -133,7 +132,10 @@ struct ChatView: View {
.textSelection(.enabled)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
- EventActionBar(event: event, our_pubkey: our_pubkey, bar: make_actionbar_model(ev: event, counter: likes))
+ EventActionBar(event: event,
+ our_pubkey: damus.pubkey,
+ bar: make_actionbar_model(ev: event, counter: damus.likes)
+ )
.environmentObject(profiles)
}
diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift
@@ -10,8 +10,7 @@ import SwiftUI
struct ChatroomView: View {
@EnvironmentObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss
- let likes: EventCounter
- let our_pubkey: String
+ let damus: DamusState
var body: some View {
ScrollViewReader { scroller in
@@ -22,8 +21,7 @@ struct ChatroomView: View {
ChatView(event: thread.events[ind],
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
next_ev: ind == count-1 ? nil : thread.events[ind+1],
- likes: likes,
- our_pubkey: our_pubkey
+ damus: damus
)
.onTapGesture {
if thread.event.id == ev.id {
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -53,7 +53,7 @@ struct EventView: View {
.environmentObject(profiles)
NavigationLink(destination: pv) {
- ProfilePicView(picture: profile?.picture, size: PFP_SIZE!, highlight: highlight)
+ ProfilePicView(picture: profile?.picture, size: PFP_SIZE!, highlight: highlight, image_cache: damus.image_cache)
}
Spacer()
diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift
@@ -36,27 +36,46 @@ struct ProfilePicView: View {
let picture: String?
let size: CGFloat
let highlight: Highlight
+ let image_cache: ImageCache
+
+ @State var img: Image? = nil
+
+ @EnvironmentObject var profiles: Profiles
var Placeholder: some View {
Color.purple.opacity(0.2)
+ .frame(width: size, height: size)
+ .cornerRadius(CORNER_RADIUS)
+ .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
+ .padding(2)
+ }
+
+ func ProfilePic(_ url: URL) -> some View {
+ let pub = load_image(cache: image_cache, from: url)
+ return Group {
+ if let img = self.img {
+ img
+ .frame(width: size, height: size)
+ .clipShape(Circle())
+ .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
+ .padding(2)
+ } else {
+ Placeholder
+ }
+ }
+ .onReceive(pub) { mimg in
+ if let img = mimg {
+ self.img = Image(uiImage: img)
+ }
+ }
}
var MainContent: some View {
Group {
- if let pic = picture.flatMap({ URL(string: $0) }) {
- AsyncImage(url: pic) { img in
- img.resizable()
- } placeholder: { Placeholder }
- .frame(width: size, height: size)
- .clipShape(Circle())
- .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
- .padding(2)
+ if let pic_url = picture.flatMap { URL(string: $0) } {
+ ProfilePic(pic_url)
} else {
Placeholder
- .frame(width: size, height: size)
- .cornerRadius(CORNER_RADIUS)
- .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
- .padding(2)
}
}
}
@@ -66,11 +85,13 @@ struct ProfilePicView: View {
}
}
+/*
struct ProfilePicView_Previews: PreviewProvider {
static var previews: some View {
ProfilePicView(picture: "http://cdn.jb55.com/img/red-me.jpg", size: 64, highlight: .none)
}
}
+ */
func hex_to_rgb(_ hex: String) -> Color {
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -24,7 +24,7 @@ struct ProfileView: View {
var TopSection: some View {
HStack(alignment: .top) {
let data = profiles.lookup(id: profile.pubkey)
- ProfilePicView(picture: data?.picture, size: 64, highlight: .custom(Color.black, 4))
+ ProfilePicView(picture: data?.picture, size: 64, highlight: .custom(Color.black, 4), image_cache: damus.image_cache)
//.border(Color.blue)
VStack(alignment: .leading) {
if let pubkey = profile.pubkey {
diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift
@@ -10,6 +10,7 @@ import SwiftUI
struct ReplyQuoteView: View {
let quoter: NostrEvent
let event_id: String
+ let image_cache: ImageCache
@EnvironmentObject var profiles: Profiles
@EnvironmentObject var thread: ThreadModel
@@ -22,7 +23,7 @@ struct ReplyQuoteView: View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
- ProfilePicView(picture: profiles.lookup(id: event.pubkey)?.picture, size: 16, highlight: .reply)
+ ProfilePicView(picture: profiles.lookup(id: event.pubkey)?.picture, size: 16, highlight: .reply, image_cache: image_cache)
Text(Profile.displayName(profile: profiles.lookup(id: event.pubkey), pubkey: event.pubkey))
.foregroundColor(.accentColor)
Text("\(format_relative_time(event.created_at))")
diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift
@@ -19,7 +19,7 @@ struct ThreadView: View {
var body: some View {
Group {
if is_chatroom {
- ChatroomView(likes: damus.likes, our_pubkey: damus.pubkey)
+ ChatroomView(damus: damus)
.navigationBarTitle("Chat")
.environmentObject(profiles)
.environmentObject(thread)