Compare commits
8 Commits
f84694b809
...
a4e7082ab8
Author | SHA1 | Date |
---|---|---|
Shadowfacts | a4e7082ab8 | |
Shadowfacts | f0b8f92791 | |
Shadowfacts | da88303a22 | |
Shadowfacts | cb5b70a23a | |
Shadowfacts | 2b5b749dc8 | |
Shadowfacts | ef00c0e2df | |
Shadowfacts | 06f7e306e0 | |
Shadowfacts | 878744b636 |
|
@ -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") {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct NodeInfo: Decodable, Sendable {
|
public struct NodeInfo: Decodable, Sendable, Equatable {
|
||||||
public let version: String
|
public let version: String
|
||||||
public let software: Software
|
public let software: Software
|
||||||
|
|
||||||
public struct Software: Decodable, Sendable {
|
public struct Software: Decodable, Sendable, Equatable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let version: String
|
public let version: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
|
@ -23,15 +23,12 @@ class MastodonController: ObservableObject {
|
||||||
@available(*, message: "do something less dumb")
|
@available(*, message: "do something less dumb")
|
||||||
static var first: MastodonController { all.first!.value }
|
static var first: MastodonController { all.first!.value }
|
||||||
|
|
||||||
|
@MainActor
|
||||||
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
||||||
if let controller = all[account] {
|
if let controller = all[account] {
|
||||||
return controller
|
return controller
|
||||||
} else {
|
} else {
|
||||||
let controller = MastodonController(instanceURL: account.instanceURL)
|
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
||||||
controller.accountInfo = account
|
|
||||||
controller.client.clientID = account.clientID
|
|
||||||
controller.client.clientSecret = account.clientSecret
|
|
||||||
controller.client.accessToken = account.accessToken
|
|
||||||
all[account] = controller
|
all[account] = controller
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -46,7 +43,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?
|
||||||
|
@ -55,8 +52,9 @@ class MastodonController: ObservableObject {
|
||||||
let client: Client!
|
let client: Client!
|
||||||
let instanceFeatures = InstanceFeatures()
|
let instanceFeatures = InstanceFeatures()
|
||||||
|
|
||||||
@Published private(set) var account: Account!
|
@Published private(set) var account: AccountMO?
|
||||||
@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]?
|
||||||
|
@ -72,27 +70,49 @@ class MastodonController: ObservableObject {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
init(instanceURL: URL, transient: Bool = false) {
|
// main-actor b/c fetchActiveAccountID and fetchActiveInstance use the viewContext
|
||||||
|
@MainActor
|
||||||
|
init(instanceURL: URL, accountInfo: UserAccountInfo?) {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.accountInfo = nil
|
self.accountInfo = accountInfo
|
||||||
self.client = Client(baseURL: instanceURL, session: .appDefault)
|
self.client = Client(baseURL: instanceURL, session: .appDefault)
|
||||||
self.transient = transient
|
self.transient = accountInfo == nil
|
||||||
|
self.persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
||||||
|
|
||||||
$instance
|
self.client.clientID = accountInfo?.clientID
|
||||||
|
self.client.clientSecret = accountInfo?.clientSecret
|
||||||
|
self.client.accessToken = accountInfo?.accessToken
|
||||||
|
|
||||||
|
if !transient {
|
||||||
|
fetchActiveAccount()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
$instanceInfo
|
||||||
|
.compactMap { $0 }
|
||||||
|
.removeDuplicates(by: { $0.version == $1.version })
|
||||||
|
.combineLatest($nodeInfo.removeDuplicates())
|
||||||
|
.sink { (instance, nodeInfo) in
|
||||||
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
|
||||||
|
@ -103,6 +123,12 @@ class MastodonController: ObservableObject {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
convenience init(instanceURL: URL, transient: Bool) {
|
||||||
|
precondition(transient, "account info must be provided if transient is false")
|
||||||
|
self.init(instanceURL: instanceURL, accountInfo: nil)
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) -> URLSessionTask? {
|
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) -> URLSessionTask? {
|
||||||
return client.run(request, completion: completion)
|
return client.run(request, completion: completion)
|
||||||
|
@ -207,8 +233,8 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) {
|
func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
|
||||||
if account != nil {
|
if let account {
|
||||||
completion?(.success(account))
|
completion?(.success(account))
|
||||||
} else {
|
} else {
|
||||||
let request = Client.getSelfAccount()
|
let request = Client.getSelfAccount()
|
||||||
|
@ -218,25 +244,25 @@ class MastodonController: ObservableObject {
|
||||||
completion?(.failure(error))
|
completion?(.failure(error))
|
||||||
|
|
||||||
case let .success(account, _):
|
case let .success(account, _):
|
||||||
DispatchQueue.main.async {
|
let context = self.persistentContainer.viewContext
|
||||||
self.account = account
|
context.perform {
|
||||||
}
|
let accountMO: AccountMO
|
||||||
self.persistentContainer.backgroundContext.perform {
|
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
||||||
if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
|
accountMO = existing
|
||||||
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
|
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
|
||||||
} else {
|
} else {
|
||||||
// the first time the user's account is added to the store,
|
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
|
||||||
// increment its reference count so that it's never removed
|
|
||||||
self.persistentContainer.addOrUpdate(account: account)
|
|
||||||
}
|
}
|
||||||
completion?(.success(account))
|
accountMO.active = true
|
||||||
|
self.account = accountMO
|
||||||
|
completion?(.success(accountMO))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnAccount() async throws -> Account {
|
func getOwnAccount() async throws -> AccountMO {
|
||||||
if let account = account {
|
if let account = account {
|
||||||
return account
|
return account
|
||||||
} else {
|
} else {
|
||||||
|
@ -332,6 +358,37 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func fetchActiveAccount() {
|
||||||
|
let req = AccountMO.fetchRequest()
|
||||||
|
req.predicate = NSPredicate(format: "active = YES")
|
||||||
|
if let activeAccount = try? persistentContainer.viewContext.fetch(req).first {
|
||||||
|
account = activeAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func fetchActiveInstance() {
|
||||||
|
if let activeInstance = try? persistentContainer.viewContext.fetch(ActiveInstance.fetchRequest()).first {
|
||||||
|
let info = InstanceInfo(activeInstance: activeInstance)
|
||||||
|
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)
|
||||||
|
@ -522,7 +579,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": [
|
||||||
|
|
|
@ -115,9 +115,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let clazz = NSClassFromString("SentryInstallation"),
|
if let clazz = NSClassFromString("SentryInstallation"),
|
||||||
let objClazz = clazz as AnyObject as? NSObjectProtocol,
|
let objClazz = clazz as AnyObject as? NSObject,
|
||||||
objClazz.responds(to: Selector(("id"))),
|
let id = objClazz.value(forKey: "id") as? String {
|
||||||
let id = objClazz.perform(Selector(("id"))).takeUnretainedValue() as? String {
|
|
||||||
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
|
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged public var acct: String
|
@NSManaged public var acct: String
|
||||||
|
/// Whether this AccountMO is the active (logged-in) account.
|
||||||
|
@NSManaged public var active: Bool
|
||||||
@NSManaged public var avatar: URL?
|
@NSManaged public var avatar: URL?
|
||||||
@NSManaged public var botCD: Bool
|
@NSManaged public var botCD: Bool
|
||||||
@NSManaged public var createdAt: Date
|
@NSManaged public var createdAt: Date
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -311,6 +311,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The caller is responsible for calling this on a queue appropriate for `context`.
|
||||||
|
func addOrUpdateSynchronously(account: Account, in context: NSManagedObjectContext) -> AccountMO {
|
||||||
|
let accountMO = self.upsert(account: account, in: context)
|
||||||
|
self.save(context: context)
|
||||||
|
self.accountSubject.send(account.id)
|
||||||
|
return accountMO
|
||||||
|
}
|
||||||
|
|
||||||
func relationship(forAccount id: String, in context: NSManagedObjectContext? = nil) -> RelationshipMO? {
|
func relationship(forAccount id: String, in context: NSManagedObjectContext? = nil) -> RelationshipMO? {
|
||||||
let context = context ?? viewContext
|
let context = context ?? viewContext
|
||||||
let request: NSFetchRequest<RelationshipMO> = RelationshipMO.fetchRequest()
|
let request: NSFetchRequest<RelationshipMO> = RelationshipMO.fetchRequest()
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
|
<attribute name="active" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="botCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="botCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -33,6 +34,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 +145,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 +152,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>
|
|
@ -27,6 +27,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||||
let controller: ComposeController
|
let controller: ComposeController
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var assetPickerCompletion: (@MainActor ([PHPickerResult]) -> Void)?
|
private var assetPickerCompletion: (@MainActor ([PHPickerResult]) -> Void)?
|
||||||
private var drawingCompletion: ((PKDrawing) -> Void)?
|
private var drawingCompletion: ((PKDrawing) -> Void)?
|
||||||
|
|
||||||
|
@ -44,7 +46,6 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||||
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
||||||
emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) }
|
emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) }
|
||||||
)
|
)
|
||||||
controller.currentAccount = mastodonController.account
|
|
||||||
|
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -58,6 +59,12 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||||
|
|
||||||
// set an initial title immediately, in case we're starting ducked
|
// set an initial title immediately, in case we're starting ducked
|
||||||
self.navigationItem.title = self.controller.navigationTitle
|
self.navigationItem.title = self.controller.navigationTitle
|
||||||
|
|
||||||
|
mastodonController.$account
|
||||||
|
.sink { [unowned self] in
|
||||||
|
self.controller.currentAccount = $0
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
|
|
@ -57,10 +57,6 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
if mastodonController.instance == nil {
|
|
||||||
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
|
||||||
}
|
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController!
|
resultsController.exploreNavigationController = self.navigationController!
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
|
@ -79,6 +75,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
mastodonController.instanceFeatures.featuresUpdated
|
||||||
|
.sink { [unowned self] in self.instanceFeaturesChanged() }
|
||||||
|
.store(in: &cancellables)
|
||||||
mastodonController.$lists
|
mastodonController.$lists
|
||||||
.sink { [unowned self] in self.reloadLists($0) }
|
.sink { [unowned self] in self.reloadLists($0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -194,7 +193,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
snapshot.appendItems([.trends], toSection: .discover)
|
snapshot.appendItems([.trends], toSection: .discover)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ownInstanceLoaded(_ instance: Instance) {
|
private func instanceFeaturesChanged() {
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
if mastodonController.instanceFeatures.trends,
|
if mastodonController.instanceFeatures.trends,
|
||||||
!snapshot.sectionIdentifiers.contains(.discover) {
|
!snapshot.sectionIdentifiers.contains(.discover) {
|
||||||
|
|
|
@ -97,10 +97,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
if mastodonController.instance == nil {
|
|
||||||
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
|
||||||
}
|
|
||||||
|
|
||||||
select(item: .tab(.timelines), animated: false)
|
select(item: .tab(.timelines), animated: false)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
|
@ -189,14 +185,6 @@ class MainSidebarViewController: UIViewController {
|
||||||
reloadSavedInstances()
|
reloadSavedInstances()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ownInstanceLoaded(_ instance: Instance) {
|
|
||||||
let prevSelected = collectionView.indexPathsForSelectedItems
|
|
||||||
|
|
||||||
if let prevSelected = prevSelected?.first {
|
|
||||||
collectionView.selectItem(at: prevSelected, animated: false, scrollPosition: .top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reloadLists(_ lists: [List], animated: Bool) {
|
private func reloadLists(_ lists: [List], animated: Bool) {
|
||||||
if let selectedItem,
|
if let selectedItem,
|
||||||
case .list(let list) = selectedItem,
|
case .list(let list) = selectedItem,
|
||||||
|
|
|
@ -150,7 +150,7 @@ class OnboardingViewController: UINavigationController {
|
||||||
mastodonController.accountInfo = tempAccountInfo
|
mastodonController.accountInfo = tempAccountInfo
|
||||||
|
|
||||||
updateStatus("Checking Credentials")
|
updateStatus("Checking Credentials")
|
||||||
let ownAccount: Account
|
let ownAccount: AccountMO
|
||||||
do {
|
do {
|
||||||
ownAccount = try await retrying("Getting own account") {
|
ownAccount = try await retrying("Getting own account") {
|
||||||
try await mastodonController.getOwnAccount()
|
try await mastodonController.getOwnAccount()
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -24,9 +24,6 @@ struct ReportView: View {
|
||||||
self.account = mastodonController.persistentContainer.account(for: report.accountID)!
|
self.account = mastodonController.persistentContainer.account(for: report.accountID)!
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self._report = StateObject(wrappedValue: report)
|
self._report = StateObject(wrappedValue: report)
|
||||||
if mastodonController.instance?.rules == nil {
|
|
||||||
report.reason = .spam
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -173,6 +170,11 @@ struct ReportView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(mastodonController.$instance) { instance in
|
||||||
|
if instance?.rules == nil {
|
||||||
|
report.reason = .spam
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendReport() {
|
private func sendReport() {
|
||||||
|
|
Loading…
Reference in New Issue