2018-08-16 07:46:19 -04:00
|
|
|
//
|
|
|
|
// MastodonController.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 8/15/18.
|
|
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2018-09-11 10:52:21 -04:00
|
|
|
import Pachyderm
|
2022-11-29 20:52:39 -05:00
|
|
|
import Combine
|
2023-03-05 14:35:25 -05:00
|
|
|
import UserAccounts
|
2023-03-05 14:52:19 -05:00
|
|
|
import InstanceFeatures
|
|
|
|
import Sentry
|
2023-04-16 13:23:13 -04:00
|
|
|
import ComposeUI
|
2018-08-16 07:46:19 -04:00
|
|
|
|
2023-04-03 22:43:01 -04:00
|
|
|
private let oauthScopes = [Scope.read, .write, .follow]
|
|
|
|
|
2020-09-21 18:04:08 -04:00
|
|
|
class MastodonController: ObservableObject {
|
2020-01-07 21:29:15 -05:00
|
|
|
|
2023-03-05 14:35:25 -05:00
|
|
|
static private(set) var all = [UserAccountInfo: MastodonController]()
|
2020-01-07 21:29:15 -05:00
|
|
|
|
|
|
|
@available(*, message: "do something less dumb")
|
|
|
|
static var first: MastodonController { all.first!.value }
|
2018-08-16 07:46:19 -04:00
|
|
|
|
2023-05-28 22:23:04 -07:00
|
|
|
@MainActor
|
2023-03-05 14:35:25 -05:00
|
|
|
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
2020-01-07 21:29:15 -05:00
|
|
|
if let controller = all[account] {
|
|
|
|
return controller
|
|
|
|
} else {
|
2023-05-28 21:28:20 -07:00
|
|
|
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
2020-01-07 21:29:15 -05:00
|
|
|
all[account] = controller
|
|
|
|
return controller
|
|
|
|
}
|
|
|
|
}
|
2018-08-16 18:55:40 -04:00
|
|
|
|
2023-03-05 14:35:25 -05:00
|
|
|
static func removeForAccount(_ account: UserAccountInfo) {
|
2023-01-27 18:42:11 -05:00
|
|
|
all.removeValue(forKey: account)
|
|
|
|
}
|
|
|
|
|
2020-05-13 18:58:11 -04:00
|
|
|
static func resetAll() {
|
|
|
|
all = [:]
|
|
|
|
}
|
|
|
|
|
2020-05-11 17:57:50 -04:00
|
|
|
private let transient: Bool
|
2023-05-28 21:50:01 -07:00
|
|
|
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
2020-04-12 11:14:10 -04:00
|
|
|
|
2020-01-07 21:29:15 -05:00
|
|
|
let instanceURL: URL
|
2023-03-05 14:35:25 -05:00
|
|
|
var accountInfo: UserAccountInfo?
|
2022-12-20 23:37:12 -05:00
|
|
|
var accountPreferences: AccountPreferences!
|
2020-01-07 21:29:15 -05:00
|
|
|
|
|
|
|
let client: Client!
|
2023-03-05 14:52:19 -05:00
|
|
|
let instanceFeatures = InstanceFeatures()
|
2018-08-16 07:46:19 -04:00
|
|
|
|
2023-05-28 22:26:46 -07:00
|
|
|
@Published private(set) var account: AccountMO?
|
2023-05-28 21:50:01 -07:00
|
|
|
@Published private(set) var instance: Instance?
|
|
|
|
@Published private(set) var instanceInfo: InstanceInfo!
|
2022-01-23 23:26:42 -05:00
|
|
|
@Published private(set) var nodeInfo: NodeInfo!
|
2022-11-19 14:08:39 -05:00
|
|
|
@Published private(set) var lists: [List] = []
|
2022-11-29 20:52:39 -05:00
|
|
|
@Published private(set) var customEmojis: [Emoji]?
|
2022-11-29 21:43:56 -05:00
|
|
|
@Published private(set) var followedHashtags: [FollowedHashtag] = []
|
2022-11-30 22:16:33 -05:00
|
|
|
@Published private(set) var filters: [FilterMO] = []
|
2022-11-29 20:52:39 -05:00
|
|
|
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
2020-09-13 15:51:06 -04:00
|
|
|
|
2022-06-30 19:26:28 -07:00
|
|
|
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
2021-04-04 15:11:29 -04:00
|
|
|
private var ownInstanceRequest: URLSessionTask?
|
|
|
|
|
2020-09-13 15:51:06 -04:00
|
|
|
var loggedIn: Bool {
|
|
|
|
accountInfo != nil
|
|
|
|
}
|
2020-01-07 21:29:15 -05:00
|
|
|
|
2023-05-28 22:23:04 -07:00
|
|
|
// main-actor b/c fetchActiveAccountID and fetchActiveInstance use the viewContext
|
|
|
|
@MainActor
|
2023-05-28 21:28:20 -07:00
|
|
|
init(instanceURL: URL, accountInfo: UserAccountInfo?) {
|
2020-01-07 21:29:15 -05:00
|
|
|
self.instanceURL = instanceURL
|
2023-05-28 21:28:20 -07:00
|
|
|
self.accountInfo = accountInfo
|
2022-01-23 10:56:36 -05:00
|
|
|
self.client = Client(baseURL: instanceURL, session: .appDefault)
|
2023-05-28 21:28:20 -07:00
|
|
|
self.transient = accountInfo == nil
|
2023-05-28 21:50:01 -07:00
|
|
|
self.persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
2023-05-28 21:28:20 -07:00
|
|
|
|
|
|
|
self.client.clientID = accountInfo?.clientID
|
|
|
|
self.client.clientSecret = accountInfo?.clientSecret
|
|
|
|
self.client.accessToken = accountInfo?.accessToken
|
2022-11-29 20:52:39 -05:00
|
|
|
|
2023-05-28 21:50:01 -07:00
|
|
|
if !transient {
|
2023-05-28 22:26:46 -07:00
|
|
|
fetchActiveAccount()
|
2023-05-28 21:50:01 -07:00
|
|
|
fetchActiveInstance()
|
|
|
|
}
|
|
|
|
|
|
|
|
$instanceInfo
|
|
|
|
.compactMap { $0 }
|
2022-11-29 20:52:39 -05:00
|
|
|
.combineLatest($nodeInfo)
|
|
|
|
.sink { [unowned self] (instance, nodeInfo) in
|
|
|
|
self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo)
|
2023-05-28 21:54:31 -07:00
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
|
|
|
|
|
|
|
$instanceInfo
|
|
|
|
.compactMap { $0 }
|
|
|
|
.removeDuplicates(by: { $0.version == $1.version })
|
|
|
|
.combineLatest($nodeInfo.removeDuplicates())
|
|
|
|
.sink { (instance, nodeInfo) in
|
2023-03-05 14:52:19 -05:00
|
|
|
setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo)
|
2022-11-29 20:52:39 -05:00
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
2022-11-29 21:43:56 -05:00
|
|
|
|
2023-05-28 21:50:01 -07:00
|
|
|
$instance
|
|
|
|
.compactMap { $0 }
|
|
|
|
.sink { [unowned self] in
|
|
|
|
self.updateActiveInstance(from: $0)
|
|
|
|
self.instanceInfo = InstanceInfo(instance: $0)
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
|
|
|
|
2023-03-05 14:52:19 -05:00
|
|
|
instanceFeatures.featuresUpdated
|
|
|
|
.filter { [unowned self] _ in self.instanceFeatures.canFollowHashtags && self.followedHashtags.isEmpty }
|
2022-11-29 21:43:56 -05:00
|
|
|
.sink { [unowned self] _ in
|
|
|
|
Task {
|
|
|
|
await self.loadFollowedHashtags()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
2018-08-16 18:55:40 -04:00
|
|
|
}
|
|
|
|
|
2023-05-28 22:23:04 -07:00
|
|
|
@MainActor
|
2023-05-28 21:28:20 -07:00
|
|
|
convenience init(instanceURL: URL, transient: Bool) {
|
|
|
|
precondition(transient, "account info must be provided if transient is false")
|
|
|
|
self.init(instanceURL: instanceURL, accountInfo: nil)
|
|
|
|
}
|
|
|
|
|
2020-10-11 22:14:45 -04:00
|
|
|
@discardableResult
|
|
|
|
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) -> URLSessionTask? {
|
|
|
|
return client.run(request, completion: completion)
|
2020-01-05 14:00:39 -05:00
|
|
|
}
|
|
|
|
|
2023-01-17 15:10:39 -05:00
|
|
|
func runResponse<Result>(_ request: Request<Result>) async -> Response<Result> {
|
|
|
|
let response = await withCheckedContinuation({ continuation in
|
2022-03-29 12:52:14 -04:00
|
|
|
client.run(request) { response in
|
2023-01-17 15:10:39 -05:00
|
|
|
continuation.resume(returning: response)
|
2022-03-29 12:52:14 -04:00
|
|
|
}
|
|
|
|
})
|
2023-01-17 15:10:39 -05:00
|
|
|
return response
|
|
|
|
}
|
|
|
|
|
2023-02-19 15:23:25 -05:00
|
|
|
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
2023-01-17 15:10:39 -05:00
|
|
|
let response = await runResponse(request)
|
2022-11-02 23:00:29 -04:00
|
|
|
try Task.checkCancellation()
|
2023-01-17 15:10:39 -05:00
|
|
|
switch response {
|
|
|
|
case .failure(let error):
|
|
|
|
throw error
|
|
|
|
case .success(let result, let pagination):
|
|
|
|
return (result, pagination)
|
|
|
|
}
|
2022-03-29 12:52:14 -04:00
|
|
|
}
|
|
|
|
|
2022-03-29 11:58:11 -04:00
|
|
|
/// - Returns: A tuple of client ID and client secret.
|
|
|
|
func registerApp() async throws -> (String, String) {
|
|
|
|
if let clientID = client.clientID,
|
|
|
|
let clientSecret = client.clientSecret {
|
|
|
|
return (clientID, clientSecret)
|
|
|
|
} else {
|
|
|
|
let app: RegisteredApplication = try await withCheckedThrowingContinuation({ continuation in
|
2023-04-03 22:43:01 -04:00
|
|
|
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: oauthScopes) { response in
|
2022-03-29 11:58:11 -04:00
|
|
|
switch response {
|
|
|
|
case .failure(let error):
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
case .success(let app, _):
|
|
|
|
continuation.resume(returning: app)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2020-01-07 21:29:15 -05:00
|
|
|
self.client.clientID = app.clientID
|
|
|
|
self.client.clientSecret = app.clientSecret
|
2022-03-29 11:58:11 -04:00
|
|
|
return (app.clientID, app.clientSecret)
|
2018-08-16 18:55:40 -04:00
|
|
|
}
|
2018-08-16 20:11:56 -04:00
|
|
|
}
|
|
|
|
|
2022-03-29 11:58:11 -04:00
|
|
|
/// - Returns: The access token
|
|
|
|
func authorize(authorizationCode: String) async throws -> String {
|
|
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
2023-04-03 22:43:01 -04:00
|
|
|
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth", scopes: oauthScopes) { response in
|
2022-03-29 11:58:11 -04:00
|
|
|
switch response {
|
|
|
|
case .failure(let error):
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
case .success(let settings, _):
|
|
|
|
self.client.accessToken = settings.accessToken
|
|
|
|
continuation.resume(returning: settings.accessToken)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2018-08-16 07:46:19 -04:00
|
|
|
}
|
|
|
|
|
2022-12-03 22:16:43 -05:00
|
|
|
@MainActor
|
|
|
|
func initialize() {
|
|
|
|
// we want this to happen immediately, and synchronously so that the filters (which don't change that often)
|
|
|
|
// are available when Filterers are constructed
|
|
|
|
loadCachedFilters()
|
2022-11-19 14:08:39 -05:00
|
|
|
|
2023-01-01 12:58:44 -05:00
|
|
|
loadAccountPreferences()
|
|
|
|
|
2023-02-25 13:55:46 -05:00
|
|
|
lists = loadCachedLists()
|
|
|
|
|
2023-01-01 12:58:44 -05:00
|
|
|
NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: persistentContainer.persistentStoreCoordinator)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [unowned self] _ in
|
|
|
|
self.loadAccountPreferences()
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
2022-12-20 23:37:12 -05:00
|
|
|
|
2022-12-03 22:16:43 -05:00
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
async let ownAccount = try getOwnAccount()
|
|
|
|
async let ownInstance = try getOwnInstance()
|
|
|
|
|
|
|
|
_ = try await (ownAccount, ownInstance)
|
|
|
|
|
|
|
|
loadLists()
|
2023-02-19 15:23:25 -05:00
|
|
|
_ = await loadFilters()
|
2022-12-03 22:16:43 -05:00
|
|
|
} catch {
|
|
|
|
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
|
|
|
|
}
|
|
|
|
}
|
2022-11-19 14:08:39 -05:00
|
|
|
}
|
|
|
|
|
2023-01-01 12:58:44 -05:00
|
|
|
@MainActor
|
|
|
|
private func loadAccountPreferences() {
|
|
|
|
if let existing = try? persistentContainer.viewContext.fetch(AccountPreferences.fetchRequest(account: accountInfo!)).first {
|
|
|
|
accountPreferences = existing
|
|
|
|
} else {
|
|
|
|
accountPreferences = AccountPreferences.default(account: accountInfo!, context: persistentContainer.viewContext)
|
|
|
|
persistentContainer.save(context: persistentContainer.viewContext)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-28 22:26:46 -07:00
|
|
|
func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
|
|
|
|
if let account {
|
2020-09-16 17:52:00 -04:00
|
|
|
completion?(.success(account))
|
2018-10-02 19:23:50 -04:00
|
|
|
} else {
|
2020-01-05 14:00:39 -05:00
|
|
|
let request = Client.getSelfAccount()
|
|
|
|
run(request) { response in
|
2020-09-16 17:52:00 -04:00
|
|
|
switch response {
|
|
|
|
case let .failure(error):
|
|
|
|
completion?(.failure(error))
|
|
|
|
|
|
|
|
case let .success(account, _):
|
2023-05-28 22:26:46 -07:00
|
|
|
let context = self.persistentContainer.viewContext
|
2023-05-28 22:23:04 -07:00
|
|
|
context.perform {
|
2023-05-28 22:26:46 -07:00
|
|
|
let accountMO: AccountMO
|
|
|
|
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
|
|
|
accountMO = existing
|
|
|
|
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
|
2020-09-16 17:52:00 -04:00
|
|
|
} else {
|
2023-05-28 22:26:46 -07:00
|
|
|
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
|
2020-09-16 17:52:00 -04:00
|
|
|
}
|
2023-05-28 22:26:46 -07:00
|
|
|
accountMO.active = true
|
|
|
|
self.account = accountMO
|
|
|
|
completion?(.success(accountMO))
|
2020-05-11 21:59:46 -04:00
|
|
|
}
|
|
|
|
}
|
2018-10-02 19:23:50 -04:00
|
|
|
}
|
2018-08-30 22:30:19 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-28 22:26:46 -07:00
|
|
|
func getOwnAccount() async throws -> AccountMO {
|
2022-03-29 11:58:11 -04:00
|
|
|
if let account = account {
|
|
|
|
return account
|
|
|
|
} else {
|
|
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
|
|
|
self.getOwnAccount { result in
|
|
|
|
continuation.resume(with: result)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-16 19:07:30 -04:00
|
|
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
2022-06-30 19:26:28 -07:00
|
|
|
getOwnInstanceInternal(retryAttempt: 0) {
|
|
|
|
if case let .success(instance) = $0 {
|
|
|
|
completion?(instance)
|
|
|
|
}
|
|
|
|
}
|
2021-04-04 15:11:29 -04:00
|
|
|
}
|
|
|
|
|
2022-06-30 19:26:28 -07:00
|
|
|
@MainActor
|
|
|
|
func getOwnInstance() async throws -> Instance {
|
|
|
|
return try await withCheckedThrowingContinuation({ continuation in
|
|
|
|
getOwnInstanceInternal(retryAttempt: 0) { result in
|
|
|
|
continuation.resume(with: result)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
|
2021-04-04 15:11:29 -04:00
|
|
|
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
|
|
|
assert(Thread.isMainThread)
|
|
|
|
|
2020-03-16 19:07:30 -04:00
|
|
|
if let instance = self.instance {
|
2022-06-30 19:26:28 -07:00
|
|
|
completion?(.success(instance))
|
2020-03-16 19:07:30 -04:00
|
|
|
} else {
|
2021-04-04 15:11:29 -04:00
|
|
|
if let completion = completion {
|
|
|
|
pendingOwnInstanceRequestCallbacks.append(completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ownInstanceRequest == nil {
|
|
|
|
let request = Client.getInstance()
|
|
|
|
ownInstanceRequest = run(request) { (response) in
|
|
|
|
switch response {
|
2022-06-30 19:26:28 -07:00
|
|
|
case .failure(let error):
|
2021-04-04 15:11:29 -04:00
|
|
|
let delay: DispatchTimeInterval
|
|
|
|
switch retryAttempt {
|
|
|
|
case 0:
|
|
|
|
delay = .seconds(1)
|
|
|
|
case 1:
|
|
|
|
delay = .seconds(5)
|
|
|
|
case 2:
|
|
|
|
delay = .seconds(30)
|
|
|
|
case 3:
|
|
|
|
delay = .seconds(60)
|
|
|
|
default:
|
|
|
|
// if we've failed four times, just give up :/
|
2022-06-30 19:26:28 -07:00
|
|
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
|
|
|
completion(.failure(error))
|
|
|
|
}
|
|
|
|
self.pendingOwnInstanceRequestCallbacks = []
|
2021-04-04 15:11:29 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
|
|
// completion is nil because in this invocation of getOwnInstanceInternal we've already added it to the pending callbacks array
|
|
|
|
self.getOwnInstanceInternal(retryAttempt: retryAttempt + 1, completion: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
case let .success(instance, _):
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.ownInstanceRequest = nil
|
|
|
|
self.instance = instance
|
|
|
|
|
|
|
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
2022-06-30 19:26:28 -07:00
|
|
|
completion(.success(instance))
|
2021-04-04 15:11:29 -04:00
|
|
|
}
|
|
|
|
self.pendingOwnInstanceRequestCallbacks = []
|
|
|
|
}
|
|
|
|
}
|
2020-09-21 18:04:08 -04:00
|
|
|
}
|
2022-01-23 23:26:42 -05:00
|
|
|
|
|
|
|
client.nodeInfo { result in
|
|
|
|
switch result {
|
|
|
|
case let .failure(error):
|
|
|
|
print("Unable to get node info: \(error)")
|
|
|
|
|
|
|
|
case let .success(nodeInfo, _):
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.nodeInfo = nodeInfo
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-16 19:07:30 -04:00
|
|
|
}
|
2018-09-29 22:20:17 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-28 21:50:01 -07:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-28 22:23:04 -07:00
|
|
|
@MainActor
|
2023-05-28 22:26:46 -07:00
|
|
|
private func fetchActiveAccount() {
|
2023-05-28 22:23:04 -07:00
|
|
|
let req = AccountMO.fetchRequest()
|
|
|
|
req.predicate = NSPredicate(format: "active = YES")
|
|
|
|
if let activeAccount = try? persistentContainer.viewContext.fetch(req).first {
|
2023-05-28 22:26:46 -07:00
|
|
|
account = activeAccount
|
2023-05-28 22:23:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@MainActor
|
2023-05-28 21:50:01 -07:00
|
|
|
private func fetchActiveInstance() {
|
2023-05-28 22:23:04 -07:00
|
|
|
if let activeInstance = try? persistentContainer.viewContext.fetch(ActiveInstance.fetchRequest()).first {
|
|
|
|
let info = InstanceInfo(activeInstance: activeInstance)
|
|
|
|
self.instanceInfo = info
|
2023-05-28 21:50:01 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-11 22:14:45 -04:00
|
|
|
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
|
|
|
|
if let emojis = self.customEmojis {
|
|
|
|
completion(emojis)
|
|
|
|
} else {
|
|
|
|
let request = Client.getCustomEmoji()
|
|
|
|
run(request) { (response) in
|
|
|
|
if case let .success(emojis, _) = response {
|
2023-01-14 11:28:33 -05:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.customEmojis = emojis
|
|
|
|
}
|
2020-10-11 22:14:45 -04:00
|
|
|
completion(emojis)
|
|
|
|
} else {
|
|
|
|
completion([])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:08:39 -05:00
|
|
|
private func loadLists() {
|
|
|
|
let req = Client.getLists()
|
|
|
|
run(req) { response in
|
|
|
|
if case .success(let lists, _) = response {
|
|
|
|
DispatchQueue.main.async {
|
2022-12-01 18:26:48 -05:00
|
|
|
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
2022-11-19 14:08:39 -05:00
|
|
|
}
|
2022-12-20 15:13:18 -05:00
|
|
|
let context = self.persistentContainer.backgroundContext
|
|
|
|
context.perform {
|
|
|
|
for list in lists {
|
|
|
|
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
|
|
|
|
existing.updateFrom(apiList: list)
|
|
|
|
} else {
|
|
|
|
_ = ListMO(apiList: list, context: context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.persistentContainer.save(context: context)
|
|
|
|
}
|
2022-11-19 14:08:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-25 13:55:46 -05:00
|
|
|
private func loadCachedLists() -> [List] {
|
|
|
|
let req = ListMO.fetchRequest()
|
|
|
|
guard let lists = try? persistentContainer.viewContext.fetch(req) else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
return lists.map {
|
|
|
|
List(id: $0.id, title: $0.title)
|
|
|
|
}.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCachedList(id: String) -> List? {
|
|
|
|
let req = ListMO.fetchRequest(id: id)
|
|
|
|
return (try? persistentContainer.viewContext.fetch(req).first).flatMap {
|
|
|
|
List(id: $0.id, title: $0.title)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:08:39 -05:00
|
|
|
@MainActor
|
|
|
|
func addedList(_ list: List) {
|
|
|
|
var new = self.lists
|
|
|
|
new.append(list)
|
2022-12-01 18:26:48 -05:00
|
|
|
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
2022-11-19 14:08:39 -05:00
|
|
|
self.lists = new
|
|
|
|
}
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
func deletedList(_ list: List) {
|
|
|
|
self.lists.removeAll(where: { $0.id == list.id })
|
|
|
|
}
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
func renamedList(_ list: List) {
|
|
|
|
var new = self.lists
|
|
|
|
if let index = new.firstIndex(where: { $0.id == list.id }) {
|
|
|
|
new[index] = list
|
|
|
|
}
|
2022-12-01 18:26:48 -05:00
|
|
|
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
2022-11-19 14:08:39 -05:00
|
|
|
self.lists = new
|
|
|
|
}
|
|
|
|
|
2022-11-29 21:43:56 -05:00
|
|
|
@MainActor
|
|
|
|
private func loadFollowedHashtags() async {
|
2022-11-29 22:52:31 -05:00
|
|
|
updateFollowedHashtags()
|
2022-11-29 21:43:56 -05:00
|
|
|
|
|
|
|
let req = Client.getFollowedHashtags()
|
|
|
|
if let (hashtags, _) = try? await run(req) {
|
|
|
|
self.persistentContainer.updateFollowedHashtags(hashtags) {
|
|
|
|
if case .success(let hashtags) = $0 {
|
|
|
|
self.followedHashtags = hashtags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-29 22:52:31 -05:00
|
|
|
@MainActor
|
|
|
|
func updateFollowedHashtags() {
|
|
|
|
followedHashtags = (try? persistentContainer.viewContext.fetch(FollowedHashtag.fetchRequest())) ?? []
|
|
|
|
}
|
|
|
|
|
2022-11-30 22:16:33 -05:00
|
|
|
@MainActor
|
|
|
|
func loadFilters() async {
|
2022-12-03 12:29:07 -05:00
|
|
|
var apiFilters: [AnyFilter]?
|
|
|
|
if instanceFeatures.filtersV2 {
|
|
|
|
let req = Client.getFiltersV2()
|
|
|
|
if let (filters, _) = try? await run(req) {
|
|
|
|
apiFilters = filters.map { .v2($0) }
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let req = Client.getFiltersV1()
|
|
|
|
if let (filters, _) = try? await run(req) {
|
|
|
|
apiFilters = filters.map { .v1($0) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let apiFilters {
|
|
|
|
self.persistentContainer.updateFilters(apiFilters) {
|
2022-11-30 22:16:33 -05:00
|
|
|
if case .success(let filters) = $0 {
|
|
|
|
self.filters = filters
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-03 22:16:43 -05:00
|
|
|
@MainActor
|
|
|
|
private func loadCachedFilters() {
|
|
|
|
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
|
|
|
|
}
|
|
|
|
|
2023-04-16 13:23:13 -04:00
|
|
|
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
|
|
|
|
var acctsToMention = [String]()
|
|
|
|
|
|
|
|
var visibility = inReplyToID != nil ? Preferences.shared.defaultReplyVisibility.resolved : Preferences.shared.defaultPostVisibility
|
|
|
|
var localOnly = false
|
|
|
|
var contentWarning = ""
|
|
|
|
|
|
|
|
if let inReplyToID = inReplyToID,
|
|
|
|
let inReplyTo = persistentContainer.status(for: inReplyToID) {
|
|
|
|
acctsToMention.append(inReplyTo.account.acct)
|
|
|
|
acctsToMention.append(contentsOf: inReplyTo.mentions.map(\.acct))
|
|
|
|
visibility = min(visibility, inReplyTo.visibility)
|
|
|
|
localOnly = instanceFeatures.localOnlyPosts && inReplyTo.localOnly
|
|
|
|
|
|
|
|
if !inReplyTo.spoilerText.isEmpty {
|
|
|
|
switch Preferences.shared.contentWarningCopyMode {
|
|
|
|
case .doNotCopy:
|
|
|
|
break
|
|
|
|
case .asIs:
|
|
|
|
contentWarning = inReplyTo.spoilerText
|
|
|
|
case .prependRe:
|
|
|
|
if inReplyTo.spoilerText.lowercased().starts(with: "re:") {
|
|
|
|
contentWarning = inReplyTo.spoilerText
|
|
|
|
} else {
|
|
|
|
contentWarning = "re: \(inReplyTo.spoilerText)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let mentioningAcct = mentioningAcct {
|
|
|
|
acctsToMention.append(mentioningAcct)
|
|
|
|
}
|
|
|
|
if let ownAccount = self.account {
|
|
|
|
acctsToMention.removeAll(where: { $0 == ownAccount.acct })
|
|
|
|
}
|
|
|
|
acctsToMention = acctsToMention.uniques()
|
|
|
|
|
2023-04-22 21:16:30 -04:00
|
|
|
return DraftsPersistentContainer.shared.createDraft(
|
2023-04-16 13:23:13 -04:00
|
|
|
accountID: accountInfo!.id,
|
|
|
|
text: text ?? acctsToMention.map { "@\($0) " }.joined(),
|
|
|
|
contentWarning: contentWarning,
|
|
|
|
inReplyToID: inReplyToID,
|
|
|
|
visibility: visibility,
|
|
|
|
localOnly: localOnly
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-05-11 09:59:57 -04:00
|
|
|
func createDraft(editing status: StatusMO, source: StatusSource) -> Draft {
|
|
|
|
precondition(status.id == source.id)
|
|
|
|
let draft = DraftsPersistentContainer.shared.createEditDraft(
|
|
|
|
accountID: accountInfo!.id,
|
|
|
|
source: source,
|
|
|
|
inReplyToID: status.inReplyToID,
|
|
|
|
visibility: status.visibility,
|
|
|
|
localOnly: status.localOnly,
|
|
|
|
attachments: status.attachments,
|
|
|
|
poll: status.poll
|
|
|
|
)
|
|
|
|
return draft
|
|
|
|
}
|
|
|
|
|
2022-11-19 14:08:39 -05:00
|
|
|
}
|
2023-03-05 14:52:19 -05:00
|
|
|
|
2023-05-28 21:50:01 -07:00
|
|
|
private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) {
|
2023-03-05 14:52:19 -05:00
|
|
|
let crumb = Breadcrumb(level: .info, category: "MastodonController")
|
|
|
|
crumb.data = [
|
|
|
|
"instance": [
|
|
|
|
"version": instance.version
|
|
|
|
],
|
|
|
|
]
|
|
|
|
if let nodeInfo {
|
|
|
|
crumb.data!["nodeInfo"] = [
|
|
|
|
"software": nodeInfo.software.name,
|
|
|
|
"version": nodeInfo.software.version,
|
|
|
|
]
|
|
|
|
}
|
|
|
|
SentrySDK.addBreadcrumb(crumb)
|
2022-11-19 14:08:39 -05:00
|
|
|
}
|