damus

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

commit cbc3c46c9d554c8829dc7ca9a9ffac48ffbcc3a1
parent 4b5c34b4e26a8dead71865d47432275cf2977fa3
Author: OlegAba <mail@olegaba.com>
Date:   Sat, 14 Jan 2023 16:56:28 -0500

Fix image crash and support SVG profile pictures

Closes: #310
Changelog-Fixed: Fixed some crashes with large images
Changelog-Added: Added SVG profile picture support

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 31+++++++++++++++++++++++++++++++
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 8++++++++
Mdamus/Info.plist | 2--
Adamus/Models/KFImageModel.swift | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/BannerImageView.swift | 30++++++++++++++++++++++--------
Mdamus/Views/ProfilePicView.swift | 84++++++++++++++++++-------------------------------------------------------------
6 files changed, 188 insertions(+), 75 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -148,6 +148,9 @@ 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; 6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; + 7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; }; + 7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; }; + 7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; }; 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; @@ -350,6 +353,7 @@ 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; }; 64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; + 7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; @@ -363,6 +367,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */, + 7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */, 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, 6C7DE41F2955169800E66263 /* Vault in Frameworks */, 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */, @@ -482,6 +488,7 @@ BA693073295D649800ADDB87 /* UserSettingsStore.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */, 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, + 7C45AE70297353390031D7BC /* KFImageModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -715,6 +722,8 @@ 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, 6C7DE41E2955169800E66263 /* Vault */, + 7C45AE6C297352F90031D7BC /* SVGKit */, + 7C45AE6E297352F90031D7BC /* SVGKitSwift */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -795,6 +804,7 @@ 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */, + 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -894,6 +904,7 @@ 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */, + 7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, @@ -1160,6 +1171,7 @@ INFOPLIST_FILE = damus/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "\"Granting Damus access to your photo library allows you to save photos."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -1200,6 +1212,7 @@ INFOPLIST_FILE = damus/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "\"Granting Damus access to your photo library allows you to save photos."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -1373,6 +1386,14 @@ minimumVersion = 1.0.0; }; }; + 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SVGKit/SVGKit"; + requirement = { + kind = revision; + revision = e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1396,6 +1417,16 @@ package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */; productName = Vault; }; + 7C45AE6C297352F90031D7BC /* SVGKit */ = { + isa = XCSwiftPackageProductDependency; + package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */; + productName = SVGKit; + }; + 7C45AE6E297352F90031D7BC /* SVGKitSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */; + productName = SVGKitSwift; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,14 @@ } }, { + "identity" : "svgkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SVGKit/SVGKit", + "state" : { + "revision" : "e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0" + } + }, + { "identity" : "vault", "kind" : "remoteSourceControl", "location" : "https://github.com/SparrowTek/Vault", diff --git a/damus/Info.plist b/damus/Info.plist @@ -15,8 +15,6 @@ </array> </dict> </array> - <key>NSPhotoLibraryAddUsageDescription</key> - <string>&quot;Granting Damus access to your photo library allows you to save photos.</string> <key>LSApplicationQueriesSchemes</key> <array> <string>river</string> diff --git a/damus/Models/KFImageModel.swift b/damus/Models/KFImageModel.swift @@ -0,0 +1,108 @@ +// +// KFImageModel.swift +// damus +// +// Created by Oleg Abalonski on 1/11/23. +// + +import Foundation +import Kingfisher +import SVGKit + +class KFImageModel: ObservableObject { + + let url: URL? + let fallbackUrl: URL? + let processor: ImageProcessor + let serializer: CacheSerializer + + @Published var refreshID = "" + + init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) { + self.url = url + self.fallbackUrl = fallbackUrl + self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize) + self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize) + } + + func refresh() -> Void { + DispatchQueue.main.async { + self.refreshID = UUID().uuidString + } + } + + func cache(_ image: UIImage, forKey key: String) -> Void { + KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in + self.refresh() + } + } + + func downloadFailed() -> Void { + guard let url = url, let fallbackUrl = fallbackUrl else { return } + + DispatchQueue.global(qos: .background).async { + KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in + + var fallbackImage: UIImage { + switch result { + case .success(let imageLoadingResult): + return imageLoadingResult.image + case .failure(let error): + print(error) + return UIImage() + } + } + + self.cache(fallbackImage, forKey: url.absoluteString) + } + } + } +} + +struct CustomImageProcessor: ImageProcessor { + + let maxSize: Int + let downsampleSize: CGSize + + let identifier = "com.damus.customimageprocessor" + + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + + switch item { + case .image(_): + // This case will never run + return DefaultImageProcessor.default.process(item: item, options: options) + case .data(let data): + + // Handle large image size + if data.count > maxSize { + return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor) + } + + // Handle SVG image + if let svgImage = SVGKImage(data: data), let image = svgImage.uiImage { + return image.kf.scaled(to: options.scaleFactor) + } + + return DefaultImageProcessor.default.process(item: item, options: options) + } + } +} + +struct CustomCacheSerializer: CacheSerializer { + + let maxSize: Int + let downsampleSize: CGSize + + func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? { + return DefaultCacheSerializer.default.data(with: image, original: original) + } + + func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? { + if data.count > maxSize { + return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor) + } + + return DefaultCacheSerializer.default.image(with: data, options: options) + } +} diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift @@ -9,29 +9,43 @@ import SwiftUI import Kingfisher struct InnerBannerImageView: View { - let url: URL? - let pubkey: String + + let defaultImage = UIImage(named: "profile-banner") ?? UIImage() + + @ObservedObject var imageModel: KFImageModel + + init(url: URL?) { + self.imageModel = KFImageModel( + url: url, + fallbackUrl: nil, + maxByteSize: 5000000, + downsampleSize: CGSize(width: 750, height: 250) + ) + } var body: some View { ZStack { Color(uiColor: .systemBackground) - if (url != nil) { - KFAnimatedImage(url) + if (imageModel.url != nil) { + KFAnimatedImage(imageModel.url) .callbackQueue(.dispatch(.global(qos: .background))) .processingQueue(.dispatch(.global(qos: .background))) - .appendProcessor(LargeImageProcessor.shared) + .serialize(by: imageModel.serializer) + .setProcessor(imageModel.processor) .configure { view in view.framePreloadCount = 1 } .placeholder { _ in - Image("profile-banner").resizable() + Color(uiColor: .secondarySystemBackground) } .scaleFactor(UIScreen.main.scale) .loadDiskFileSynchronously() .fade(duration: 0.1) + .onFailureImage(defaultImage) + .id(imageModel.refreshID) } else { - Image("profile-banner").resizable() + Image(uiImage: defaultImage).resizable() } } } @@ -50,7 +64,7 @@ struct BannerImageView: View { } var body: some View { - InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles), pubkey: pubkey) + InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles)) .onReceive(handle_notify(.profile_updated)) { notif in let updated = notif.object as! ProfileUpdate diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -33,13 +33,23 @@ func pfp_line_width(_ h: Highlight) -> CGFloat { } struct InnerProfilePicView: View { - let url: URL? - let fallbackUrl: URL? let pubkey: String let size: CGFloat let highlight: Highlight - @State private var refreshID = UUID().uuidString + @ObservedObject var imageModel: KFImageModel + + init(url: URL?, fallbackUrl: URL?, pubkey: String, size: CGFloat, highlight: Highlight) { + self.pubkey = pubkey + self.size = size + self.highlight = highlight + self.imageModel = KFImageModel( + url: url, + fallbackUrl: fallbackUrl, + maxByteSize: 1000000, + downsampleSize: CGSize(width: 200, height: 200) + ) + } var PlaceholderColor: Color { return id_to_color(pubkey) @@ -57,10 +67,12 @@ struct InnerProfilePicView: View { ZStack { Color(uiColor: .systemBackground) - KFAnimatedImage(url) + KFAnimatedImage(imageModel.url) .callbackQueue(.dispatch(.global(qos: .background))) .processingQueue(.dispatch(.global(qos: .background))) - .appendProcessor(LargeImageProcessor.shared) + .serialize(by: imageModel.serializer) + .setProcessor(imageModel.processor) + .cacheOriginalImage() .configure { view in view.framePreloadCount = 1 } @@ -71,42 +83,13 @@ struct InnerProfilePicView: View { .loadDiskFileSynchronously() .fade(duration: 0.1) .onFailure { _ in - setFallbackImage() + imageModel.downloadFailed() } + .id(imageModel.refreshID) } .frame(width: size, height: size) .clipShape(Circle()) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) - .id(refreshID) - } - - func refreshView() -> Void { - refreshID = UUID().uuidString - } - - func setFallbackImage() -> Void { - - guard let url = url, let fallbackUrl = fallbackUrl else { return } - - KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in - - func fallbackImage() -> UIImage { - switch result { - case .success(let imageLoadingResult): - return imageLoadingResult.image - case .failure(let error): - print(error) - return UIImage() - } - } - - // Kingfisher ID format for caching when using a custom processor - let processorIdentifier = "|>" + LargeImageProcessor.shared.identifier - - KingfisherManager.shared.cache.store(fallbackImage(), forKey: url.absoluteString, processorIdentifier: processorIdentifier) { _ in - refreshView() - } - } } } @@ -142,35 +125,6 @@ struct ProfilePicView: View { } } -struct LargeImageProcessor: ImageProcessor { - - static let shared = LargeImageProcessor() - - let identifier = "com.damus.largeimageprocessor" - let maxSize: Int = 1000000 - let downsampleSize = CGSize(width: 200, height: 200) - - func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { - - switch item { - case .image(let image): - guard let data = image.kf.data(format: .unknown) else { - return nil - } - - if data.count > maxSize { - return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor) - } - return image - case .data(let data): - if data.count > maxSize { - return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor) - } - return KFCrossPlatformImage(data: data) - } - } -} - func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> URL { let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) if let url = URL(string: pic) {