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:
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>"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) {