commit 5018b9aa1e5487d3ecb130c4249a80b680f00223
parent 1f6657e471b55195543fff062769d5241512d4f8
Author: OlegAba <mail@olegaba.com>
Date: Wed, 15 Feb 2023 22:14:59 -0500
Added a 20MB content length limit for all image files
Changelog-Changed: Added a 20MB content length limit for all image files
Closes: #335
Diffstat:
7 files changed, 175 insertions(+), 223 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -206,9 +206,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 */; };
- 7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
+ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.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 */; };
@@ -515,9 +515,9 @@
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.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>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
+ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.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>"; };
@@ -663,7 +663,6 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
- 7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
@@ -789,6 +788,7 @@
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
+ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1257,7 +1257,6 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
- 7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1265,6 +1264,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
+ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift
@@ -66,20 +66,11 @@ struct ImageContextMenuModifier: ViewModifier {
private struct ImageContainerView: View {
- @ObservedObject var imageModel: KFImageModel
+ let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
- init(url: URL?) {
- self.imageModel = KFImageModel(
- url: url,
- fallbackUrl: nil,
- maxByteSize: 2000000, // 2 MB
- downsampleSize: CGSize(width: 400, height: 400)
- )
- }
-
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -91,30 +82,17 @@ private struct ImageContainerView: View {
var body: some View {
- KFAnimatedImage(imageModel.url)
- .callbackQueue(.dispatch(.global(qos: .background)))
- .processingQueue(.dispatch(.global(qos: .background)))
- .cacheOriginalImage()
+ KFAnimatedImage(url)
+ .imageContext(.note)
.configure { view in
- view.framePreloadCount = 1
+ view.framePreloadCount = 3
}
- .scaleFactor(UIScreen.main.scale)
- .loadDiskFileSynchronously()
- .fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
- .onFailure { _ in
- imageModel.downloadFailed()
- }
- .id(imageModel.refreshID)
.clipped()
- .modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
+ .modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
- ShareSheet(activityItems: [imageModel.url])
+ ShareSheet(activityItems: [url])
}
-
- // TODO: Update ImageCarousel with serializer and processor
- // .serialize(by: imageModel.serializer)
- // .setProcessor(imageModel.processor)
}
}
@@ -221,12 +199,8 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
- .callbackQueue(.dispatch(.global(qos: .background)))
- .processingQueue(.dispatch(.global(qos: .background)))
+ .imageContext(.note)
.cancelOnDisappear(true)
- .backgroundDecode()
- .cacheOriginalImage()
- .scaleFactor(UIScreen.main.scale)
.configure { view in
view.framePreloadCount = 3
}
diff --git a/damus/Models/KFImageModel.swift b/damus/Models/KFImageModel.swift
@@ -1,114 +0,0 @@
-//
-// KFImageModel.swift
-// damus
-//
-// Created by Oleg Abalonski on 1/11/23.
-//
-
-import UIKit
-import Kingfisher
-
-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 dataString = String(data: data, encoding: .utf8),
- let svg = SVG(dataString) {
-
- let render = UIGraphicsImageRenderer(size: svg.size)
- let image = render.image { context in
- svg.draw(in: context.cgContext)
- }
-
- 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/Util/KFOptionSetter+.swift b/damus/Util/KFOptionSetter+.swift
@@ -0,0 +1,148 @@
+//
+// KFOptionSetter+.swift
+// damus
+//
+// Created by Oleg Abalonski on 2/15/23.
+//
+
+import UIKit
+import Kingfisher
+
+extension KFOptionSetter {
+
+ func imageContext(_ imageContext: ImageContext) -> Self {
+ options.callbackQueue = .dispatch(.global(qos: .background))
+ options.processingQueue = .dispatch(.global(qos: .background))
+ options.downloader = CustomImageDownloader.shared
+ options.backgroundDecode = true
+ options.cacheOriginalImage = true
+ options.scaleFactor = UIScreen.main.scale
+
+ options.processor = CustomImageProcessor(
+ maxSize: imageContext.maxMebibyteSize(),
+ downsampleSize: imageContext.downsampleSize()
+ )
+
+ options.cacheSerializer = CustomCacheSerializer(
+ maxSize: imageContext.maxMebibyteSize(),
+ downsampleSize: imageContext.downsampleSize()
+ )
+
+ return self
+ }
+
+ func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
+ guard let url = fallbackUrl, let key = cacheKey else { return self }
+ let imageResource = ImageResource(downloadURL: url, cacheKey: key)
+ let source = imageResource.convertToSource()
+ options.alternativeSources = [source]
+
+ return self
+ }
+}
+
+let MAX_FILE_SIZE = 20_971_520 // 20MiB
+
+enum ImageContext {
+ case pfp
+ case banner
+ case note
+
+ func maxMebibyteSize() -> Int {
+ switch self {
+ case .pfp:
+ return 5_242_880 // 5Mib
+ case .banner, .note:
+ return 20_971_520 // 20MiB
+ }
+ }
+
+ func downsampleSize() -> CGSize {
+ switch self {
+ case .pfp:
+ return CGSize(width: 200, height: 200)
+ case .banner:
+ return CGSize(width: 750, height: 250)
+ case .note:
+ return CGSize(width: 500, height: 500)
+ }
+ }
+}
+
+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 dataString = String(data: data, encoding: .utf8),
+ let svg = SVG(dataString) {
+
+ let render = UIGraphicsImageRenderer(size: svg.size)
+ let image = render.image { context in
+ svg.draw(in: context.cgContext)
+ }
+
+ 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)
+ }
+}
+
+class CustomSessionDelegate: SessionDelegate {
+ override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
+ let contentLength = response.expectedContentLength
+
+ // Content-Length header is optional (-1 when missing)
+ if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
+ return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
+ }
+
+ super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
+ }
+}
+
+class CustomImageDownloader: ImageDownloader {
+
+ static let shared = CustomImageDownloader(name: "shared")
+
+ override init(name: String) {
+ super.init(name: name)
+ sessionDelegate = CustomSessionDelegate()
+ }
+}
diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift
@@ -10,40 +10,23 @@ import Kingfisher
struct InnerBannerImageView: View {
+ let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
-
- @ObservedObject var imageModel: KFImageModel
-
- init(url: URL?) {
- self.imageModel = KFImageModel(
- url: url,
- fallbackUrl: nil,
- maxByteSize: 20_971_520, // 20 MiB
- downsampleSize: CGSize(width: 750, height: 250)
- )
- }
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
- if (imageModel.url != nil) {
- KFAnimatedImage(imageModel.url)
- .callbackQueue(.dispatch(.global(qos: .background)))
- .processingQueue(.dispatch(.global(qos: .background)))
- .serialize(by: imageModel.serializer)
- .setProcessor(imageModel.processor)
- .cacheOriginalImage()
+ if (url != nil) {
+ KFAnimatedImage(url)
+ .imageContext(.banner)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Color(uiColor: .secondarySystemBackground)
}
- .scaleFactor(UIScreen.main.scale)
- .loadDiskFileSynchronously()
.onFailureImage(defaultImage)
- .id(imageModel.refreshID)
} else {
Image(uiImage: defaultImage).resizable()
}
diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift
@@ -33,23 +33,12 @@ 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
-
- @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: 5_242_880, // 5Mib
- downsampleSize: CGSize(width: 200, height: 200)
- )
- }
var PlaceholderColor: Color {
return id_to_color(pubkey)
@@ -67,26 +56,16 @@ struct InnerProfilePicView: View {
ZStack {
Color(uiColor: .systemBackground)
- KFAnimatedImage(imageModel.url)
- .callbackQueue(.dispatch(.global(qos: .background)))
- .processingQueue(.dispatch(.global(qos: .background)))
+ KFAnimatedImage(url)
+ .imageContext(.pfp)
+ .onFailure(fallbackUrl: fallbackUrl, cacheKey: url?.absoluteString)
.cancelOnDisappear(true)
- .backgroundDecode()
- .serialize(by: imageModel.serializer)
- .setProcessor(imageModel.processor)
- .cacheOriginalImage()
- .scaleFactor(UIScreen.main.scale)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Placeholder
}
- .onFailure { error in
- if error.isTaskCancelled { return }
- imageModel.downloadFailed()
- }
- .id(imageModel.refreshID)
}
.frame(width: size, height: size)
.clipShape(Circle())
diff --git a/damus/Views/ProfileZoomView.swift b/damus/Views/ProfileZoomView.swift
@@ -9,20 +9,11 @@ import Kingfisher
private struct ImageContainerView: View {
- @ObservedObject var imageModel: KFImageModel
+ let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
- init(url: URL?) {
- self.imageModel = KFImageModel(
- url: url,
- fallbackUrl: nil,
- maxByteSize: 2000000, // 2 MB
- downsampleSize: CGSize(width: 400, height: 400)
- )
- }
-
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -34,25 +25,16 @@ private struct ImageContainerView: View {
var body: some View {
- KFAnimatedImage(imageModel.url)
- .callbackQueue(.dispatch(.global(qos: .background)))
- .processingQueue(.dispatch(.global(qos: .background)))
- .cacheOriginalImage()
+ KFAnimatedImage(url)
+ .imageContext(.pfp)
.configure { view in
- view.framePreloadCount = 1
+ view.framePreloadCount = 3
}
- .scaleFactor(UIScreen.main.scale)
- .loadDiskFileSynchronously()
- .fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
- .onFailure { _ in
- imageModel.downloadFailed()
- }
- .id(imageModel.refreshID)
.clipShape(Circle())
- .modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
+ .modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
- ShareSheet(activityItems: [imageModel.url])
+ ShareSheet(activityItems: [url])
}
}
}