From ef00c0e2df0e190edb50b18c25a48f31a0cb4ce4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 28 May 2023 21:50:01 -0700 Subject: [PATCH] Cache own instance in CoreData See #251 --- .../InstanceFeatures/InstanceFeatures.swift | 2 +- .../InstanceFeatures/InstanceInfo.swift | 34 ++++++++++++ .../Sources/Pachyderm/Model/Instance.swift | 8 +-- ShareExtension/ShareMastodonContext.swift | 2 +- Tusker.xcodeproj/project.pbxproj | 4 ++ Tusker/API/MastodonController.swift | 55 +++++++++++++++---- Tusker/CoreData/ActiveInstance.swift | 49 +++++++++++++++++ .../Tusker.xcdatamodel/contents | 9 ++- .../Report/ReportSelectRulesView.swift | 2 +- 9 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceInfo.swift create mode 100644 Tusker/CoreData/ActiveInstance.swift diff --git a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift index 68080c6e..bf392f14 100644 --- a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift +++ b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift @@ -135,7 +135,7 @@ public class InstanceFeatures: ObservableObject { public init() { } - public func update(instance: Instance, nodeInfo: NodeInfo?) { + public func update(instance: InstanceInfo, nodeInfo: NodeInfo?) { let ver = instance.version.lowercased() // check glitch first b/c it still reports "mastodon" as the software in nodeinfo if ver.contains("glitch") { diff --git a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceInfo.swift b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceInfo.swift new file mode 100644 index 00000000..d4a5e984 --- /dev/null +++ b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceInfo.swift @@ -0,0 +1,34 @@ +// +// InstanceInfo.swift +// InstanceFeatures +// +// Created by Shadowfacts on 5/28/23. +// + +import Foundation +import Pachyderm + +public struct InstanceInfo { + public let version: String + public let maxStatusCharacters: Int? + public let configuration: Instance.Configuration? + public let pollsConfiguration: Instance.PollsConfiguration? + + public init(version: String, maxStatusCharacters: Int?, configuration: Instance.Configuration?, pollsConfiguration: Instance.PollsConfiguration?) { + self.version = version + self.maxStatusCharacters = maxStatusCharacters + self.configuration = configuration + self.pollsConfiguration = pollsConfiguration + } +} + +extension InstanceInfo { + public init(instance: Instance) { + self.init( + version: instance.version, + maxStatusCharacters: instance.maxStatusCharacters, + configuration: instance.configuration, + pollsConfiguration: instance.pollsConfiguration + ) + } +} diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift index b3474209..b90ca413 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift @@ -107,7 +107,7 @@ extension Instance { } extension Instance { - public struct Configuration: Decodable, Sendable { + public struct Configuration: Codable, Sendable { public let statuses: StatusesConfiguration public let mediaAttachments: MediaAttachmentsConfiguration /// Use Instance.pollsConfiguration to support older instance that don't have this nested @@ -122,7 +122,7 @@ extension Instance { } extension Instance { - public struct StatusesConfiguration: Decodable, Sendable { + public struct StatusesConfiguration: Codable, Sendable { public let maxCharacters: Int public let maxMediaAttachments: Int public let charactersReservedPerURL: Int @@ -136,7 +136,7 @@ extension Instance { } extension Instance { - public struct MediaAttachmentsConfiguration: Decodable, Sendable { + public struct MediaAttachmentsConfiguration: Codable, Sendable { public let supportedMIMETypes: [String] public let imageSizeLimit: Int public let imageMatrixLimit: Int @@ -156,7 +156,7 @@ extension Instance { } extension Instance { - public struct PollsConfiguration: Decodable, Sendable { + public struct PollsConfiguration: Codable, Sendable { public let maxOptions: Int public let maxCharactersPerOption: Int public let minExpiration: TimeInterval diff --git a/ShareExtension/ShareMastodonContext.swift b/ShareExtension/ShareMastodonContext.swift index 829b34fd..f9914a12 100644 --- a/ShareExtension/ShareMastodonContext.swift +++ b/ShareExtension/ShareMastodonContext.swift @@ -40,7 +40,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject { } }) guard let instance = await instance else { return } - self.instanceFeatures.update(instance: instance, nodeInfo: await nodeInfo) + self.instanceFeatures.update(instance: InstanceInfo(instance: instance), nodeInfo: await nodeInfo) } Task { @MainActor in diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7afdd863..a63dc391 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */; }; D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */; }; D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */; }; + D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; @@ -426,6 +427,7 @@ D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusCollectionViewCell.swift; sourceTree = ""; }; D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardCollectionViewCell.swift; sourceTree = ""; }; D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestedProfileCardCollectionViewCell.xib; sourceTree = ""; }; + D608470E2A245D1F00C17380 /* ActiveInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveInstance.swift; sourceTree = ""; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = ""; }; D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = ""; }; D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = ""; }; @@ -954,6 +956,7 @@ D6A3A3812956123A0036B6EF /* TimelinePosition.swift */, D68A76E229524D2A001DA1B3 /* ListMO.swift */, D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */, + D608470E2A245D1F00C17380 /* ActiveInstance.swift */, D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */, D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */, ); @@ -1959,6 +1962,7 @@ D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, + D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */, D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index 1790720c..c9c4e2a0 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -42,7 +42,7 @@ class MastodonController: ObservableObject { } private let transient: Bool - private(set) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) + nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient) let instanceURL: URL var accountInfo: UserAccountInfo? @@ -52,7 +52,8 @@ class MastodonController: ObservableObject { let instanceFeatures = InstanceFeatures() @Published private(set) var account: Account! - @Published private(set) var instance: Instance! + @Published private(set) var instance: Instance? + @Published private(set) var instanceInfo: InstanceInfo! @Published private(set) var nodeInfo: NodeInfo! @Published private(set) var lists: [List] = [] @Published private(set) var customEmojis: [Emoji]? @@ -73,26 +74,33 @@ class MastodonController: ObservableObject { self.accountInfo = accountInfo self.client = Client(baseURL: instanceURL, session: .appDefault) self.transient = accountInfo == nil + self.persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) self.client.clientID = accountInfo?.clientID self.client.clientSecret = accountInfo?.clientSecret self.client.accessToken = accountInfo?.accessToken - $instance + if !transient { + fetchActiveInstance() + } + + $instanceInfo + .compactMap { $0 } .combineLatest($nodeInfo) - .compactMap { (instance, nodeInfo) in - if let instance { - return (instance, nodeInfo) - } else { - return nil - } - } .sink { [unowned self] (instance, nodeInfo) in self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo) setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) } .store(in: &cancellables) + $instance + .compactMap { $0 } + .sink { [unowned self] in + self.updateActiveInstance(from: $0) + self.instanceInfo = InstanceInfo(instance: $0) + } + .store(in: &cancellables) + instanceFeatures.featuresUpdated .filter { [unowned self] _ in self.instanceFeatures.canFollowHashtags && self.followedHashtags.isEmpty } .sink { [unowned self] _ in @@ -337,6 +345,31 @@ class MastodonController: ObservableObject { } } + private func updateActiveInstance(from instance: Instance) { + persistentContainer.performBackgroundTask { context in + if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first { + existing.update(from: instance) + } else { + let new = ActiveInstance(context: context) + new.update(from: instance) + } + if context.hasChanges { + try? context.save() + } + } + } + + private func fetchActiveInstance() { + persistentContainer.performBackgroundTask { context in + if let activeInstance = try? context.fetch(ActiveInstance.fetchRequest()).first { + let info = InstanceInfo(activeInstance: activeInstance) + DispatchQueue.main.async { + self.instanceInfo = info + } + } + } + } + func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) { if let emojis = self.customEmojis { completion(emojis) @@ -527,7 +560,7 @@ class MastodonController: ObservableObject { } -private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) { +private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) { let crumb = Breadcrumb(level: .info, category: "MastodonController") crumb.data = [ "instance": [ diff --git a/Tusker/CoreData/ActiveInstance.swift b/Tusker/CoreData/ActiveInstance.swift new file mode 100644 index 00000000..1a2a36b9 --- /dev/null +++ b/Tusker/CoreData/ActiveInstance.swift @@ -0,0 +1,49 @@ +// +// ActiveInstance.swift +// Tusker +// +// Created by Shadowfacts on 5/28/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import Foundation +import CoreData +import Pachyderm +import InstanceFeatures + +@objc(ActiveInstance) +public final class ActiveInstance: NSManagedObject { + + @nonobjc class public func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ActiveInstance") + } + + @NSManaged public var version: String + @NSManaged public var maxStatusCharacters: Int + @NSManaged private var configurationData: Data? + @NSManaged private var pollsConfigurationData: Data? + + @LazilyDecoding(from: \ActiveInstance.configurationData, fallback: nil) + public var configuration: Instance.Configuration? + + @LazilyDecoding(from: \ActiveInstance.pollsConfigurationData, fallback: nil) + public var pollsConfiguration: Instance.PollsConfiguration? + + func update(from instance: Instance) { + self.version = instance.version + self.maxStatusCharacters = instance.maxStatusCharacters ?? 500 + self.configuration = instance.configuration + self.pollsConfiguration = instance.pollsConfiguration + } +} + +extension InstanceInfo { + init(activeInstance: ActiveInstance) { + self.init( + version: activeInstance.version, + maxStatusCharacters: activeInstance.maxStatusCharacters, + configuration: activeInstance.configuration, + pollsConfiguration: activeInstance.pollsConfiguration + ) + } +} diff --git a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents index 32419bcb..5aa247c3 100644 --- a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents +++ b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents @@ -33,6 +33,12 @@ + + + + + + @@ -138,7 +144,6 @@ - @@ -146,5 +151,7 @@ + + \ No newline at end of file diff --git a/Tusker/Screens/Report/ReportSelectRulesView.swift b/Tusker/Screens/Report/ReportSelectRulesView.swift index 703695e5..9c10280e 100644 --- a/Tusker/Screens/Report/ReportSelectRulesView.swift +++ b/Tusker/Screens/Report/ReportSelectRulesView.swift @@ -31,7 +31,7 @@ struct ReportSelectRulesView: View { } var body: some View { - List(mastodonController.instance.rules!) { rule in + List(mastodonController.instance!.rules!) { rule in Button { if selectedRuleIDs.contains(rule.id) { selectedRuleIDs.removeAll(where: { $0 == rule.id })