@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ struct DamusState {
|
||||
let pubkey: String
|
||||
let likes: EventCounter
|
||||
let boosts: EventCounter
|
||||
let image_cache: ImageCache
|
||||
|
||||
}
|
||||
|
||||
@@ -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] = [:]
|
||||
|
||||
28
damus/Util/ImageCache.swift
Normal file
28
damus/Util/ImageCache.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user