diff --git a/Caches/ImageCacheConfiguration.swift b/Caches/ImageCacheConfiguration.swift new file mode 100644 index 0000000..c2de384 --- /dev/null +++ b/Caches/ImageCacheConfiguration.swift @@ -0,0 +1,32 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import Kingfisher +import ServiceLayer + +struct ImageCacheConfiguration { + private let environment: AppEnvironment + + init(environment: AppEnvironment) { + self.environment = environment + } +} + +extension ImageCacheConfiguration { + func configure() throws { + KingfisherManager.shared.cache = try ImageCache( + name: Self.name, + cacheDirectoryURL: Self.directoryURL) + try KingfisherManager.shared.defaultOptions = [ + .cacheSerializer(ImageCacheSerializer(service: .init(environment: environment))) + ] + } +} + +private extension ImageCacheConfiguration { + static let name = "Images" + static let directoryURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppEnvironment.appGroup)? + .appendingPathComponent("Library") + .appendingPathComponent("Caches") +} diff --git a/Caches/ImageCacheSerializer.swift b/Caches/ImageCacheSerializer.swift new file mode 100644 index 0000000..62604db --- /dev/null +++ b/Caches/ImageCacheSerializer.swift @@ -0,0 +1,27 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import Kingfisher +import ServiceLayer + +struct ImageCacheSerializer { + private let service: ImageSerializationService + + init(service: ImageSerializationService) { + self.service = service + } +} + +extension ImageCacheSerializer: CacheSerializer { + func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + guard let data = image.kf.data(format: original?.kf.imageFormat ?? .unknown) else { return nil } + + return try? service.serialize(data: data) + } + + func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + guard let deserialized = try? service.deserialize(data: data) else { return nil } + + return KingfisherWrapper.image(data: deserialized, options: .init()) + } +} diff --git a/Extensions/KingfisherOptionsInfo+Extensions.swift b/Extensions/KingfisherOptionsInfo+Extensions.swift deleted file mode 100644 index f734e4e..0000000 --- a/Extensions/KingfisherOptionsInfo+Extensions.swift +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Kingfisher -import SwiftUI - -extension KingfisherOptionsInfo { - static func downsampled(size: CGSize, scaleFactor: CGFloat, rounded: Bool = true) -> Self { - var processor: ImageProcessor = DownsamplingImageProcessor(size: size) - - if rounded { - processor = processor.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5))) - } - - return [ - .processor(processor), - .scaleFactor(scaleFactor), - .cacheOriginalImage, - .cacheSerializer(FormatIndicatedCacheSerializer.png) - ] - } - - static func downsampled(dimension: CGFloat, scaleFactor: CGFloat, rounded: Bool = true) -> Self { - downsampled(size: CGSize(width: dimension, height: dimension), scaleFactor: scaleFactor, rounded: rounded) - } -} - -extension KFOptionSetter { - func downsampled(size: CGSize, scaleFactor: CGFloat, rounded: Bool = true) -> Self { - var processor: ImageProcessor = DownsamplingImageProcessor(size: size) - - if rounded { - processor = processor.append(another: RoundCornerImageProcessor(radius: .widthFraction(0.5))) - } - options.processor = processor - options.scaleFactor = scaleFactor - options.cacheOriginalImage = true - options.cacheSerializer = FormatIndicatedCacheSerializer.png - - return self - } - - func downsampled(dimension: CGFloat, scaleFactor: CGFloat, rounded: Bool = true) -> Self { - downsampled(size: CGSize(width: dimension, height: dimension), scaleFactor: scaleFactor, rounded: rounded) - } -} diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index dbf41e4..b2115d0 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -31,6 +31,14 @@ D021A69025C3E4B8008A0C0D /* EmojiContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */; }; D021A69525C3E4C1008A0C0D /* EmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7F125B13E57006DF726 /* EmojiView.swift */; }; D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */; }; + D025B14625C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */; }; + D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */; }; + D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */; }; + D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */; }; + D025B15B25C4EA7D001C69A8 /* ImageCacheConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */; }; + D025B16025C4EA81001C69A8 /* ImageCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */; }; + D025B16A25C4EB18001C69A8 /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D025B16925C4EB18001C69A8 /* ServiceLayer */; }; + D025B17025C4EB58001C69A8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = D025B16F25C4EB58001C69A8 /* Kingfisher */; }; D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; }; D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */; }; D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; }; @@ -46,7 +54,6 @@ D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; }; - D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D03D87F425C23C44004DCBB2 /* SecondaryNavigationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03D87F325C23C44004DCBB2 /* SecondaryNavigationTitleView.swift */; }; D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D04F9E8D259E9C950081B0C9 /* ViewModels */; }; D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */; }; @@ -113,8 +120,6 @@ D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; }; D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; }; - D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB972501C0FC002C1B13 /* Secrets */; }; - D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB992501C15F002C1B13 /* Mastodon */; }; D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; }; D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; }; D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42524F76169001EBDBB /* TableView.swift */; }; @@ -130,7 +135,6 @@ D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46A24F76169001EBDBB /* String+Extensions.swift */; }; D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */; }; D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; - D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; }; D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; }; D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; }; @@ -223,6 +227,8 @@ D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityContentConfiguration.swift; sourceTree = ""; }; D021A62B25C38570008A0C0D /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; D021A63525C38ADB008A0C0D /* AcknowledgmentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgmentsView.swift; sourceTree = ""; }; + D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheSerializer.swift; sourceTree = ""; }; + D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheConfiguration.swift; sourceTree = ""; }; D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = ""; }; D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = ""; }; @@ -315,7 +321,6 @@ D0C7D46A24F76169001EBDBB /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = ""; }; D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; - D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = ""; }; D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = ""; }; @@ -380,8 +385,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */, - D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */, + D025B17025C4EB58001C69A8 /* Kingfisher in Frameworks */, + D025B16A25C4EB18001C69A8 /* ServiceLayer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -672,7 +677,6 @@ D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */, D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */, D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */, - D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */, D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */, D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */, @@ -706,6 +710,8 @@ D0FE1C9625368A15003EF1EB /* Caches */ = { isa = PBXGroup; children = ( + D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */, + D025B14525C4D26A001C69A8 /* ImageCacheSerializer.swift */, D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */, ); path = Caches; @@ -795,8 +801,8 @@ ); name = "Notification Service Extension"; packageProductDependencies = ( - D0BECB972501C0FC002C1B13 /* Secrets */, - D0BECB992501C15F002C1B13 /* Mastodon */, + D025B16925C4EB18001C69A8 /* ServiceLayer */, + D025B16F25C4EB58001C69A8 /* Kingfisher */, ); productName = "Notification Service Extension"; productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; @@ -977,7 +983,6 @@ D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */, D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */, D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */, - D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */, D08B8D72254246E200B1EBEF /* PollView.swift in Sources */, D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */, D0EA59402522AC8700804347 /* CardView.swift in Sources */, @@ -985,6 +990,7 @@ D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */, D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */, + D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */, D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */, D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */, D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */, @@ -998,6 +1004,7 @@ D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */, D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */, D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */, + D025B14625C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */, D0B8510C25259E56004E0744 /* LoadMoreTableViewCell.swift in Sources */, D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */, D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */, @@ -1038,13 +1045,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */, D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */, D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */, D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */, D021A69025C3E4B8008A0C0D /* EmojiContentConfiguration.swift in Sources */, D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */, D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */, + D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */, D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */, D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */, D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */, @@ -1064,6 +1071,7 @@ D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */, D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */, D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */, + D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */, D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */, D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */, D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */, @@ -1076,6 +1084,8 @@ buildActionMask = 2147483647; files = ( D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */, + D025B15B25C4EA7D001C69A8 /* ImageCacheConfiguration.swift in Sources */, + D025B16025C4EA81001C69A8 /* ImageCacheSerializer.swift in Sources */, D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1479,18 +1489,19 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D025B16925C4EB18001C69A8 /* ServiceLayer */ = { + isa = XCSwiftPackageProductDependency; + productName = ServiceLayer; + }; + D025B16F25C4EB58001C69A8 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = D0F5880325A7E4C500E3A49C /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; D04F9E8D259E9C950081B0C9 /* ViewModels */ = { isa = XCSwiftPackageProductDependency; productName = ViewModels; }; - D0BECB972501C0FC002C1B13 /* Secrets */ = { - isa = XCSwiftPackageProductDependency; - productName = Secrets; - }; - D0BECB992501C15F002C1B13 /* Mastodon */ = { - isa = XCSwiftPackageProductDependency; - productName = Mastodon; - }; D0E2C1D024FD97F000854680 /* ViewModels */ = { isa = XCSwiftPackageProductDependency; productName = ViewModels; diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index 98dafe6..345d840 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -2,11 +2,20 @@ import CryptoKit import Keychain +import Kingfisher import Mastodon import Secrets +import ServiceLayer import UserNotifications final class NotificationService: UNNotificationServiceExtension { + private let environment = AppEnvironment.live( + userNotificationCenter: .current(), + reduceMotion: { false }) + + override init() { + super.init() + } var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? @@ -37,16 +46,33 @@ final class NotificationService: UNNotificationServiceExtension { let fileName = pushNotification.icon.lastPathComponent let fileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(fileName) - do { - let iconData = try Data(contentsOf: pushNotification.icon) + KingfisherManager.shared.retrieveImage(with: pushNotification.icon) { + switch $0 { + case let .success(result): + let format: ImageFormat - try iconData.write(to: fileURL) - bestAttemptContent.attachments = [try UNNotificationAttachment(identifier: fileName, url: fileURL)] - } catch { - // no-op + switch fileURL.pathExtension.lowercased() { + case "jpg", "jpeg": + format = .JPEG + case "gif": + format = .GIF + case "png": + format = .PNG + default: + format = .unknown + } + + do { + try result.image.kf.data(format: format)?.write(to: fileURL) + bestAttemptContent.attachments = [try UNNotificationAttachment(identifier: fileName, url: fileURL)] + contentHandler(bestAttemptContent) + } catch { + contentHandler(bestAttemptContent) + } + case .failure: + contentHandler(bestAttemptContent) + } } - - contentHandler(bestAttemptContent) } override func serviceExtensionTimeWillExpire() { diff --git a/Secrets/Sources/Secrets/Secrets.swift b/Secrets/Sources/Secrets/Secrets.swift index 72e56c7..accc789 100644 --- a/Secrets/Sources/Secrets/Secrets.swift +++ b/Secrets/Sources/Secrets/Secrets.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import Base16 +import CryptoKit import Foundation import Keychain @@ -32,6 +33,7 @@ public extension Secrets { case pushKey case pushAuth case databaseKey + case imageCacheKey case identityDatabaseName } } @@ -46,7 +48,7 @@ extension Secrets.Item { case key } - // Note `databaseKey` is a generic password and not a key + // Note `databaseKey` and `imageCacheKey` are stored as generic passwords, not keys var kind: Kind { switch self { case .pushKey: return .key @@ -68,6 +70,18 @@ public extension Secrets { } } + static func imageCacheKey(keychain: Keychain.Type) throws -> Data { + do { + return try unscopedItem(.imageCacheKey, keychain: keychain) + } catch SecretsError.itemAbsent { + let imageCacheKey = Data(SymmetricKey(size: .bits256).withUnsafeBytes(Array.init)) + + try setUnscoped(imageCacheKey, forItem: .imageCacheKey, keychain: keychain) + + return imageCacheKey + } + } + // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key static func databaseKey(identityId: UUID?, keychain: Keychain.Type) throws -> String { let passphraseData: Data diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ImageSerializationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ImageSerializationService.swift new file mode 100644 index 0000000..b1de102 --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Services/ImageSerializationService.swift @@ -0,0 +1,23 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import CryptoKit +import Foundation +import Secrets + +public struct ImageSerializationService { + private let key: SymmetricKey + + public init(environment: AppEnvironment) throws { + key = try SymmetricKey(data: Secrets.imageCacheKey(keychain: environment.keychain)) + } +} + +public extension ImageSerializationService { + func serialize(data: Data) throws -> Data { + try ChaChaPoly.seal(data, using: key).combined + } + + func deserialize(data: Data) throws -> Data { + try ChaChaPoly.open(.init(combined: data), using: key) + } +} diff --git a/Share Extension/ShareExtensionNavigationViewController.swift b/Share Extension/ShareExtensionNavigationViewController.swift index c57bdb9..265d0b0 100644 --- a/Share Extension/ShareExtensionNavigationViewController.swift +++ b/Share Extension/ShareExtensionNavigationViewController.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import AVKit import Combine import ServiceLayer import SwiftUI @@ -7,14 +8,17 @@ import ViewModels @objc(ShareExtensionNavigationViewController) class ShareExtensionNavigationViewController: UINavigationController { - private let viewModel = ShareExtensionNavigationViewModel( - environment: .live( - userNotificationCenter: .current(), - reduceMotion: { UIAccessibility.isReduceMotionEnabled })) + private let environment = AppEnvironment.live( + userNotificationCenter: .current(), + reduceMotion: { UIAccessibility.isReduceMotionEnabled }) override func viewDidLoad() { super.viewDidLoad() + try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) + try? ImageCacheConfiguration(environment: environment).configure() + + let viewModel = ShareExtensionNavigationViewModel(environment: environment) let newStatusViewModel: NewStatusViewModel do { diff --git a/System/AppDelegate.swift b/System/AppDelegate.swift index a49f2a3..4332bcb 100644 --- a/System/AppDelegate.swift +++ b/System/AppDelegate.swift @@ -1,6 +1,5 @@ // Copyright © 2020 Metabolist. All rights reserved. -import AVKit import Combine import UIKit @@ -28,8 +27,6 @@ extension AppDelegate: UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { self.application = application - try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) - return true } diff --git a/System/MetatextApp.swift b/System/MetatextApp.swift index 52433ff..1254c6b 100644 --- a/System/MetatextApp.swift +++ b/System/MetatextApp.swift @@ -1,22 +1,39 @@ // Copyright © 2020 Metabolist. All rights reserved. +import AVKit +import Kingfisher +import ServiceLayer import SwiftUI import ViewModels @main struct MetatextApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + private let environment = AppEnvironment.live( + userNotificationCenter: .current(), + reduceMotion: { UIAccessibility.isReduceMotionEnabled }) + + init() { + try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) + try? ImageCacheConfiguration(environment: environment).configure() + } var body: some Scene { WindowGroup { RootView( // swiftlint:disable force_try viewModel: try! RootViewModel( - environment: .live( - userNotificationCenter: .current(), - reduceMotion: { UIAccessibility.isReduceMotionEnabled }), + environment: environment, registerForRemoteNotifications: appDelegate.registerForRemoteNotifications)) // swiftlint:enable force_try } } } + +private extension MetatextApp { + static let imageCacheName = "Images" + static let imageCacheDirectoryURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppEnvironment.appGroup)? + .appendingPathComponent("Library") + .appendingPathComponent("Caches") +} diff --git a/Views/UIKit/CompositionView.swift b/Views/UIKit/CompositionView.swift index 5e3914a..84b8249 100644 --- a/Views/UIKit/CompositionView.swift +++ b/Views/UIKit/CompositionView.swift @@ -253,11 +253,7 @@ private extension CompositionView { } if let image = identity.image { - KingfisherManager.shared.retrieveImage( - with: image, - options: KingfisherOptionsInfo.downsampled( - dimension: .barButtonItemDimension, - scaleFactor: UIScreen.main.scale)) { + KingfisherManager.shared.retrieveImage(with: image) { if case let .success(value) = $0 { action.image = value.image } diff --git a/Views/UIKit/SecondaryNavigationButton.swift b/Views/UIKit/SecondaryNavigationButton.swift index c838ed9..15e3bd6 100644 --- a/Views/UIKit/SecondaryNavigationButton.swift +++ b/Views/UIKit/SecondaryNavigationButton.swift @@ -14,9 +14,6 @@ final class SecondaryNavigationButton: UIBarButtonItem { let button = UIButton( type: .custom, primaryAction: UIAction { _ in viewModel.presentingSecondaryNavigation = true }) - let downsampled = KingfisherOptionsInfo.downsampled( - dimension: .barButtonItemDimension, - scaleFactor: UIScreen.main.scale) button.imageView?.contentMode = .scaleAspectFill button.layer.cornerRadius = .barButtonItemDimension / 2 @@ -33,8 +30,7 @@ final class SecondaryNavigationButton: UIBarButtonItem { button.kf.setImage( with: $0.image, for: .normal, - placeholder: UIImage(systemName: "line.horizontal.3"), - options: downsampled) + placeholder: UIImage(systemName: "line.horizontal.3")) } .store(in: &cancellables) @@ -46,7 +42,7 @@ final class SecondaryNavigationButton: UIBarButtonItem { } if let image = identity.image { - KingfisherManager.shared.retrieveImage(with: image, options: downsampled) { + KingfisherManager.shared.retrieveImage(with: image) { if case let .success(value) = $0 { action.image = value.image }