damus

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

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:
Mdamus.xcodeproj/project.pbxproj | 8++++----
Mdamus/Components/ImageCarousel.swift | 40+++++++---------------------------------
Ddamus/Models/KFImageModel.swift | 114-------------------------------------------------------------------------------
Adamus/Util/KFOptionSetter+.swift | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/BannerImageView.swift | 25++++---------------------
Mdamus/Views/ProfilePicView.swift | 33++++++---------------------------
Mdamus/Views/ProfileZoomView.swift | 30++++++------------------------
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]) } } }