Cache own instance in CoreData

See #251
This commit is contained in:
Shadowfacts 2023-05-28 21:50:01 -07:00
parent 06f7e306e0
commit ef00c0e2df
9 changed files with 146 additions and 19 deletions

View File

@ -135,7 +135,7 @@ public class InstanceFeatures: ObservableObject {
public init() { public init() {
} }
public func update(instance: Instance, nodeInfo: NodeInfo?) { public func update(instance: InstanceInfo, nodeInfo: NodeInfo?) {
let ver = instance.version.lowercased() let ver = instance.version.lowercased()
// check glitch first b/c it still reports "mastodon" as the software in nodeinfo // check glitch first b/c it still reports "mastodon" as the software in nodeinfo
if ver.contains("glitch") { if ver.contains("glitch") {

View File

@ -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
)
}
}

View File

@ -107,7 +107,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct Configuration: Decodable, Sendable { public struct Configuration: Codable, Sendable {
public let statuses: StatusesConfiguration public let statuses: StatusesConfiguration
public let mediaAttachments: MediaAttachmentsConfiguration public let mediaAttachments: MediaAttachmentsConfiguration
/// Use Instance.pollsConfiguration to support older instance that don't have this nested /// Use Instance.pollsConfiguration to support older instance that don't have this nested
@ -122,7 +122,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct StatusesConfiguration: Decodable, Sendable { public struct StatusesConfiguration: Codable, Sendable {
public let maxCharacters: Int public let maxCharacters: Int
public let maxMediaAttachments: Int public let maxMediaAttachments: Int
public let charactersReservedPerURL: Int public let charactersReservedPerURL: Int
@ -136,7 +136,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct MediaAttachmentsConfiguration: Decodable, Sendable { public struct MediaAttachmentsConfiguration: Codable, Sendable {
public let supportedMIMETypes: [String] public let supportedMIMETypes: [String]
public let imageSizeLimit: Int public let imageSizeLimit: Int
public let imageMatrixLimit: Int public let imageMatrixLimit: Int
@ -156,7 +156,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct PollsConfiguration: Decodable, Sendable { public struct PollsConfiguration: Codable, Sendable {
public let maxOptions: Int public let maxOptions: Int
public let maxCharactersPerOption: Int public let maxCharactersPerOption: Int
public let minExpiration: TimeInterval public let minExpiration: TimeInterval

View File

@ -40,7 +40,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
} }
}) })
guard let instance = await instance else { return } 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 Task { @MainActor in

View File

@ -29,6 +29,7 @@
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */; }; D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */; };
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */; }; D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */; };
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */; }; 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 */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; 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 = "<group>"; }; D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusCollectionViewCell.swift; sourceTree = "<group>"; };
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardCollectionViewCell.swift; sourceTree = "<group>"; }; D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardCollectionViewCell.swift; sourceTree = "<group>"; };
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestedProfileCardCollectionViewCell.xib; sourceTree = "<group>"; }; D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestedProfileCardCollectionViewCell.xib; sourceTree = "<group>"; };
D608470E2A245D1F00C17380 /* ActiveInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveInstance.swift; sourceTree = "<group>"; };
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; }; D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; }; D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; };
@ -954,6 +956,7 @@
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */, D6A3A3812956123A0036B6EF /* TimelinePosition.swift */,
D68A76E229524D2A001DA1B3 /* ListMO.swift */, D68A76E229524D2A001DA1B3 /* ListMO.swift */,
D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */, D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */,
D608470E2A245D1F00C17380 /* ActiveInstance.swift */,
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */, D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */, D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
); );
@ -1959,6 +1962,7 @@
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */, D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */,
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */, D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,

View File

@ -42,7 +42,7 @@ class MastodonController: ObservableObject {
} }
private let transient: Bool 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 let instanceURL: URL
var accountInfo: UserAccountInfo? var accountInfo: UserAccountInfo?
@ -52,7 +52,8 @@ class MastodonController: ObservableObject {
let instanceFeatures = InstanceFeatures() let instanceFeatures = InstanceFeatures()
@Published private(set) var account: Account! @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 nodeInfo: NodeInfo!
@Published private(set) var lists: [List] = [] @Published private(set) var lists: [List] = []
@Published private(set) var customEmojis: [Emoji]? @Published private(set) var customEmojis: [Emoji]?
@ -73,26 +74,33 @@ class MastodonController: ObservableObject {
self.accountInfo = accountInfo self.accountInfo = accountInfo
self.client = Client(baseURL: instanceURL, session: .appDefault) self.client = Client(baseURL: instanceURL, session: .appDefault)
self.transient = accountInfo == nil self.transient = accountInfo == nil
self.persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
self.client.clientID = accountInfo?.clientID self.client.clientID = accountInfo?.clientID
self.client.clientSecret = accountInfo?.clientSecret self.client.clientSecret = accountInfo?.clientSecret
self.client.accessToken = accountInfo?.accessToken self.client.accessToken = accountInfo?.accessToken
$instance if !transient {
fetchActiveInstance()
}
$instanceInfo
.compactMap { $0 }
.combineLatest($nodeInfo) .combineLatest($nodeInfo)
.compactMap { (instance, nodeInfo) in
if let instance {
return (instance, nodeInfo)
} else {
return nil
}
}
.sink { [unowned self] (instance, nodeInfo) in .sink { [unowned self] (instance, nodeInfo) in
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo) self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo)
} }
.store(in: &cancellables) .store(in: &cancellables)
$instance
.compactMap { $0 }
.sink { [unowned self] in
self.updateActiveInstance(from: $0)
self.instanceInfo = InstanceInfo(instance: $0)
}
.store(in: &cancellables)
instanceFeatures.featuresUpdated instanceFeatures.featuresUpdated
.filter { [unowned self] _ in self.instanceFeatures.canFollowHashtags && self.followedHashtags.isEmpty } .filter { [unowned self] _ in self.instanceFeatures.canFollowHashtags && self.followedHashtags.isEmpty }
.sink { [unowned self] _ in .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) { func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
if let emojis = self.customEmojis { if let emojis = self.customEmojis {
completion(emojis) 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") let crumb = Breadcrumb(level: .info, category: "MastodonController")
crumb.data = [ crumb.data = [
"instance": [ "instance": [

View File

@ -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<ActiveInstance> {
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
)
}
}

View File

@ -33,6 +33,12 @@
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/> <attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/>
</entity> </entity>
<entity name="ActiveInstance" representedClassName="ActiveInstance" syncable="YES">
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
<attribute name="maxStatusCharacters" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="pollsConfigurationData" optional="YES" attributeType="Binary"/>
<attribute name="version" optional="YES" attributeType="String"/>
</entity>
<entity name="Filter" representedClassName="FilterMO" syncable="YES"> <entity name="Filter" representedClassName="FilterMO" syncable="YES">
<attribute name="action" attributeType="String" defaultValueString="warn"/> <attribute name="action" attributeType="String" defaultValueString="warn"/>
<attribute name="context" attributeType="String"/> <attribute name="context" attributeType="String"/>
@ -138,7 +144,6 @@
<memberEntity name="TimelinePosition"/> <memberEntity name="TimelinePosition"/>
</configuration> </configuration>
<configuration name="Local"> <configuration name="Local">
<memberEntity name="Account"/>
<memberEntity name="Filter"/> <memberEntity name="Filter"/>
<memberEntity name="FilterKeyword"/> <memberEntity name="FilterKeyword"/>
<memberEntity name="FollowedHashtag"/> <memberEntity name="FollowedHashtag"/>
@ -146,5 +151,7 @@
<memberEntity name="Status"/> <memberEntity name="Status"/>
<memberEntity name="TimelineState"/> <memberEntity name="TimelineState"/>
<memberEntity name="List"/> <memberEntity name="List"/>
<memberEntity name="Account"/>
<memberEntity name="ActiveInstance"/>
</configuration> </configuration>
</model> </model>

View File

@ -31,7 +31,7 @@ struct ReportSelectRulesView: View {
} }
var body: some View { var body: some View {
List(mastodonController.instance.rules!) { rule in List(mastodonController.instance!.rules!) { rule in
Button { Button {
if selectedRuleIDs.contains(rule.id) { if selectedRuleIDs.contains(rule.id) {
selectedRuleIDs.removeAll(where: { $0 == rule.id }) selectedRuleIDs.removeAll(where: { $0 == rule.id })