forked from shadowfacts/Tusker
More strict concurrency fixes
This commit is contained in:
parent
ba60f92223
commit
fc26c9fb54
|
@ -15,7 +15,8 @@ public protocol ComposeMastodonContext {
|
||||||
var instanceFeatures: InstanceFeatures { get }
|
var instanceFeatures: InstanceFeatures { get }
|
||||||
|
|
||||||
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
|
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
|
||||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void)
|
|
||||||
|
func getCustomEmojis() async -> [Emoji]
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func searchCachedAccounts(query: String) -> [AccountProtocol]
|
func searchCachedAccounts(query: String) -> [AccountProtocol]
|
||||||
|
|
|
@ -44,11 +44,7 @@ class AutocompleteEmojisController: ViewController {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func queryChanged(_ query: String) async {
|
private func queryChanged(_ query: String) async {
|
||||||
var emojis = await withCheckedContinuation { continuation in
|
var emojis = await composeController.mastodonController.getCustomEmojis()
|
||||||
composeController.mastodonController.getCustomEmojis {
|
|
||||||
continuation.resume(returning: $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import WebURL
|
||||||
/**
|
/**
|
||||||
The base Mastodon API client.
|
The base Mastodon API client.
|
||||||
*/
|
*/
|
||||||
public class Client {
|
public struct Client: Sendable {
|
||||||
|
|
||||||
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
|
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
|
||||||
|
|
||||||
|
@ -20,8 +20,6 @@ public class Client {
|
||||||
let session: URLSession
|
let session: URLSession
|
||||||
|
|
||||||
public var accessToken: String?
|
public var accessToken: String?
|
||||||
|
|
||||||
public var appID: String?
|
|
||||||
public var clientID: String?
|
public var clientID: String?
|
||||||
public var clientSecret: String?
|
public var clientSecret: String?
|
||||||
|
|
||||||
|
@ -61,9 +59,11 @@ public class Client {
|
||||||
return encoder
|
return encoder
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
|
public init(baseURL: URL, accessToken: String? = nil, clientID: String? = nil, clientSecret: String? = nil, session: URLSession = .shared) {
|
||||||
self.baseURL = baseURL
|
self.baseURL = baseURL
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.clientID = clientID
|
||||||
|
self.clientSecret = clientSecret
|
||||||
self.session = session
|
self.session = session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,14 +150,7 @@ public class Client {
|
||||||
"scopes" => scopes.scopeString,
|
"scopes" => scopes.scopeString,
|
||||||
"website" => website?.absoluteString
|
"website" => website?.absoluteString
|
||||||
]))
|
]))
|
||||||
run(request) { result in
|
run(request, completion: completion)
|
||||||
defer { completion(result) }
|
|
||||||
guard case let .success(application, _) = result else { return }
|
|
||||||
|
|
||||||
self.appID = application.id
|
|
||||||
self.clientID = application.clientID
|
|
||||||
self.clientSecret = application.clientSecret
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getAccessToken(authorizationCode: String, redirectURI: String, scopes: [Scope], completion: @escaping Callback<LoginSettings>) {
|
public func getAccessToken(authorizationCode: String, redirectURI: String, scopes: [Scope], completion: @escaping Callback<LoginSettings>) {
|
||||||
|
@ -169,12 +162,7 @@ public class Client {
|
||||||
"redirect_uri" => redirectURI,
|
"redirect_uri" => redirectURI,
|
||||||
"scope" => scopes.scopeString,
|
"scope" => scopes.scopeString,
|
||||||
]))
|
]))
|
||||||
run(request) { result in
|
run(request, completion: completion)
|
||||||
defer { completion(result) }
|
|
||||||
guard case let .success(loginSettings, _) = result else { return }
|
|
||||||
|
|
||||||
self.accessToken = loginSettings.accessToken
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func revokeAccessToken() async throws {
|
public func revokeAccessToken() async throws {
|
||||||
|
@ -198,21 +186,16 @@ public class Client {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
|
public func nodeInfo() async throws -> NodeInfo {
|
||||||
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
||||||
run(wellKnown) { result in
|
let wellKnownResults = try await run(wellKnown).0
|
||||||
switch result {
|
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
||||||
case let .failure(error):
|
let href = WebURL(url.href),
|
||||||
completion(.failure(error))
|
href.host == WebURL(self.baseURL)?.host {
|
||||||
|
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
||||||
case let .success(wellKnown, _):
|
return try await run(nodeInfo).0
|
||||||
if let url = wellKnown.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
} else {
|
||||||
let href = WebURL(url.href),
|
throw NodeInfoError.noWellKnownLink
|
||||||
href.host == WebURL(self.baseURL)?.host {
|
|
||||||
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
|
||||||
self.run(nodeInfo, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,4 +583,15 @@ extension Client {
|
||||||
case invalidModel(Swift.Error)
|
case invalidModel(Swift.Error)
|
||||||
case mastodonError(Int, String)
|
case mastodonError(Int, String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum NodeInfoError: LocalizedError {
|
||||||
|
case noWellKnownLink
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .noWellKnownLink:
|
||||||
|
return "No well-known link"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,16 +29,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
async let instance = try? await run(Client.getInstanceV1()).0
|
async let instance = try? await run(Client.getInstanceV1()).0
|
||||||
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
|
async let nodeInfo = try? await client.nodeInfo()
|
||||||
self.client.nodeInfo { response in
|
|
||||||
switch response {
|
|
||||||
case .success(let nodeInfo, _):
|
|
||||||
continuation.resume(returning: nodeInfo)
|
|
||||||
case .failure(_):
|
|
||||||
continuation.resume(returning: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
guard let instance = await instance else { return }
|
guard let instance = await instance else { return }
|
||||||
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
|
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
|
||||||
}
|
}
|
||||||
|
@ -66,15 +57,13 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
|
func getCustomEmojis() async -> [Emoji] {
|
||||||
if let customEmojis {
|
if let customEmojis {
|
||||||
completion(customEmojis)
|
return customEmojis
|
||||||
} else {
|
} else {
|
||||||
Task.detached { @MainActor in
|
let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? []
|
||||||
let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? []
|
self.customEmojis = emojis
|
||||||
self.customEmojis = emojis
|
return emojis
|
||||||
completion(emojis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -314,6 +314,7 @@
|
||||||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
||||||
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
|
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
|
||||||
|
D6DEBA8D2B6579830008629A /* MainThreadBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEBA8C2B6579830008629A /* MainThreadBox.swift */; };
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
||||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
|
||||||
|
@ -722,6 +723,7 @@
|
||||||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
||||||
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
||||||
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadBox.swift; sourceTree = "<group>"; };
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1502,6 +1504,7 @@
|
||||||
D61F75BC293D099600C0B37F /* Lazy.swift */,
|
D61F75BC293D099600C0B37F /* Lazy.swift */,
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D61DC84528F498F200B82C6E /* Logging.swift */,
|
D61DC84528F498F200B82C6E /* Logging.swift */,
|
||||||
|
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||||
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
|
@ -2149,6 +2152,7 @@
|
||||||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */,
|
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */,
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */,
|
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */,
|
||||||
|
D6DEBA8D2B6579830008629A /* MainThreadBox.swift in Sources */,
|
||||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||||
|
|
|
@ -15,8 +15,10 @@ import InstanceFeatures
|
||||||
import Sentry
|
import Sentry
|
||||||
#endif
|
#endif
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
|
import OSLog
|
||||||
|
|
||||||
private let oauthScopes = [Scope.read, .write, .follow]
|
private let oauthScopes = [Scope.read, .write, .follow]
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
||||||
|
|
||||||
class MastodonController: ObservableObject {
|
class MastodonController: ObservableObject {
|
||||||
|
|
||||||
|
@ -48,26 +50,30 @@ class MastodonController: ObservableObject {
|
||||||
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
||||||
|
|
||||||
let instanceURL: URL
|
let instanceURL: URL
|
||||||
var accountInfo: UserAccountInfo?
|
let accountInfo: UserAccountInfo?
|
||||||
var accountPreferences: AccountPreferences!
|
private(set) var accountPreferences: AccountPreferences!
|
||||||
|
|
||||||
let client: Client!
|
private(set) var client: Client!
|
||||||
let instanceFeatures = InstanceFeatures()
|
let instanceFeatures = InstanceFeatures()
|
||||||
|
|
||||||
@Published private(set) var account: AccountMO?
|
@MainActor @Published private(set) var account: AccountMO?
|
||||||
@Published private(set) var instance: InstanceV1?
|
@MainActor @Published private(set) var instance: InstanceV1?
|
||||||
@Published private var instanceV2: InstanceV2?
|
@MainActor @Published private var instanceV2: InstanceV2?
|
||||||
@Published private(set) var instanceInfo: InstanceInfo!
|
@MainActor @Published private(set) var instanceInfo: InstanceInfo!
|
||||||
@Published private(set) var nodeInfo: NodeInfo!
|
@MainActor @Published private(set) var nodeInfo: NodeInfo!
|
||||||
@Published private(set) var lists: [List] = []
|
@MainActor @Published private(set) var lists: [List] = []
|
||||||
@Published private(set) var customEmojis: [Emoji]?
|
@MainActor @Published private(set) var customEmojis: [Emoji]?
|
||||||
@Published private(set) var followedHashtags: [FollowedHashtag] = []
|
@MainActor @Published private(set) var followedHashtags: [FollowedHashtag] = []
|
||||||
@Published private(set) var filters: [FilterMO] = []
|
@MainActor @Published private(set) var filters: [FilterMO] = []
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
|
@MainActor
|
||||||
private var ownInstanceRequest: URLSessionTask?
|
private var fetchOwnInstanceTask: Task<InstanceV1, any Error>?
|
||||||
|
@MainActor
|
||||||
|
private var fetchOwnAccountTask: Task<MainThreadBox<AccountMO>, any Error>?
|
||||||
|
@MainActor
|
||||||
|
private var fetchCustomEmojisTask: Task<[Emoji], Never>?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
|
@ -222,22 +228,37 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
async let ownAccount = try getOwnAccount()
|
_ = try await getOwnAccount()
|
||||||
async let ownInstance = try getOwnInstance()
|
|
||||||
|
|
||||||
_ = try await (ownAccount, ownInstance)
|
|
||||||
|
|
||||||
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
|
|
||||||
async let _ = try? getOwnInstanceV2()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLists()
|
|
||||||
_ = await loadFilters()
|
|
||||||
await loadServerPreferences()
|
|
||||||
} catch {
|
} catch {
|
||||||
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
|
logger.error("Fetch own account failed: \(String(describing: error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let instanceTask = Task {
|
||||||
|
do {
|
||||||
|
_ = try await getOwnInstance()
|
||||||
|
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
|
||||||
|
_ = try? await getOwnInstanceV2()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Fetch instance failed: \(String(describing: error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
_ = await instanceTask.value
|
||||||
|
await loadLists()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
_ = await instanceTask.value
|
||||||
|
_ = await loadFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
_ = await instanceTask.value
|
||||||
|
await loadServerPreferences()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -250,36 +271,43 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
|
func getOwnAccount(completion: (@Sendable (Result<AccountMO, any Error>) -> Void)? = nil) {
|
||||||
if let account {
|
Task.detached {
|
||||||
completion?(.success(account))
|
do {
|
||||||
} else {
|
let account = try await self.getOwnAccount()
|
||||||
let request = Client.getSelfAccount()
|
completion?(.success(account))
|
||||||
run(request) { response in
|
} catch {
|
||||||
switch response {
|
completion?(.failure(error))
|
||||||
case let .failure(error):
|
|
||||||
completion?(.failure(error))
|
|
||||||
|
|
||||||
case let .success(account, _):
|
|
||||||
let context = self.persistentContainer.viewContext
|
|
||||||
context.perform {
|
|
||||||
let accountMO: AccountMO
|
|
||||||
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
|
||||||
accountMO = existing
|
|
||||||
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
|
|
||||||
} else {
|
|
||||||
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
|
|
||||||
}
|
|
||||||
accountMO.active = true
|
|
||||||
self.account = accountMO
|
|
||||||
completion?(.success(accountMO))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func getOwnAccount() async throws -> AccountMO {
|
func getOwnAccount() async throws -> AccountMO {
|
||||||
|
if let account {
|
||||||
|
return account
|
||||||
|
} else if let fetchOwnAccountTask {
|
||||||
|
return try await fetchOwnAccountTask.value.value
|
||||||
|
} else {
|
||||||
|
let task = Task {
|
||||||
|
let account = try await run(Client.getSelfAccount()).0
|
||||||
|
|
||||||
|
let context = persistentContainer.viewContext
|
||||||
|
return await context.perform {
|
||||||
|
let accountMO: AccountMO
|
||||||
|
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
||||||
|
accountMO = existing
|
||||||
|
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
|
||||||
|
} else {
|
||||||
|
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
|
||||||
|
}
|
||||||
|
// TODO: is AccountMO.active used anywhere?
|
||||||
|
accountMO.active = true
|
||||||
|
self.account = accountMO
|
||||||
|
return MainThreadBox(value: accountMO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let account = account {
|
if let account = account {
|
||||||
return account
|
return account
|
||||||
} else {
|
} else {
|
||||||
|
@ -291,90 +319,60 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
|
func getOwnInstance(completion: (@Sendable (InstanceV1) -> Void)? = nil) {
|
||||||
getOwnInstanceInternal(retryAttempt: 0) {
|
Task.detached {
|
||||||
if case let .success(instance) = $0 {
|
let ownInstance = try? await self.getOwnInstance()
|
||||||
completion?(instance)
|
if let ownInstance {
|
||||||
|
completion?(ownInstance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func getOwnInstance() async throws -> InstanceV1 {
|
func getOwnInstance() async throws -> InstanceV1 {
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
if let instance {
|
||||||
getOwnInstanceInternal(retryAttempt: 0) { result in
|
return instance
|
||||||
continuation.resume(with: result)
|
} else if let fetchOwnInstanceTask {
|
||||||
}
|
return try await fetchOwnInstanceTask.value
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<InstanceV1, Client.Error>) -> Void)?) {
|
|
||||||
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
|
||||||
assert(Thread.isMainThread)
|
|
||||||
|
|
||||||
if let instance = self.instance {
|
|
||||||
completion?(.success(instance))
|
|
||||||
} else {
|
} else {
|
||||||
if let completion = completion {
|
let task = Task {
|
||||||
pendingOwnInstanceRequestCallbacks.append(completion)
|
let instance = try await retrying("Fetch Own Instance") {
|
||||||
}
|
try await run(Client.getInstanceV1()).0
|
||||||
|
}
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
if ownInstanceRequest == nil {
|
Task {
|
||||||
let request = Client.getInstanceV1()
|
await fetchNodeInfo()
|
||||||
ownInstanceRequest = run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case .failure(let error):
|
|
||||||
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 :/
|
|
||||||
for completion in self.pendingOwnInstanceRequestCallbacks {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
self.pendingOwnInstanceRequestCallbacks = []
|
|
||||||
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 {
|
|
||||||
completion(.success(instance))
|
|
||||||
}
|
|
||||||
self.pendingOwnInstanceRequestCallbacks = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.nodeInfo { result in
|
return instance
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
print("Unable to get node info: \(error)")
|
|
||||||
|
|
||||||
case let .success(nodeInfo, _):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.nodeInfo = nodeInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fetchOwnInstanceTask = task
|
||||||
|
return try await task.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func fetchNodeInfo() async {
|
||||||
|
if let nodeInfo = try? await client.nodeInfo() {
|
||||||
|
self.nodeInfo = nodeInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retrying<T: Sendable>(_ label: StaticString, action: () async throws -> T) async throws -> T {
|
||||||
|
for attempt in 0..<4 {
|
||||||
|
do {
|
||||||
|
return try await action()
|
||||||
|
} catch {
|
||||||
|
let seconds = UInt64(truncating: pow(2, attempt) as NSNumber)
|
||||||
|
logger.error("\(label, privacy: .public) failed, waiting \(seconds, privacy: .public)s before retrying. Reason: \(String(describing: error))")
|
||||||
|
try! await Task.sleep(nanoseconds: seconds * NSEC_PER_SEC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return try await action()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func getOwnInstanceV2() async throws {
|
private func getOwnInstanceV2() async throws {
|
||||||
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
|
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
|
||||||
}
|
}
|
||||||
|
@ -425,43 +423,43 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
|
@MainActor
|
||||||
if let emojis = self.customEmojis {
|
func getCustomEmojis() async -> [Emoji] {
|
||||||
completion(emojis)
|
if let customEmojis {
|
||||||
|
return customEmojis
|
||||||
|
} else if let fetchCustomEmojisTask {
|
||||||
|
return await fetchCustomEmojisTask.value
|
||||||
} else {
|
} else {
|
||||||
let request = Client.getCustomEmoji()
|
let task = Task {
|
||||||
run(request) { (response) in
|
let emojis = (try? await run(Client.getCustomEmoji()).0) ?? []
|
||||||
if case let .success(emojis, _) = response {
|
customEmojis = emojis
|
||||||
DispatchQueue.main.async {
|
return emojis
|
||||||
self.customEmojis = emojis
|
|
||||||
}
|
|
||||||
completion(emojis)
|
|
||||||
} else {
|
|
||||||
completion([])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fetchCustomEmojisTask = task
|
||||||
|
return await task.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadLists() {
|
private func loadLists() async {
|
||||||
let req = Client.getLists()
|
let req = Client.getLists()
|
||||||
run(req) { response in
|
guard let (lists, _) = try? await run(req) else {
|
||||||
if case .success(let lists, _) = response {
|
return
|
||||||
DispatchQueue.main.async {
|
}
|
||||||
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
let sorted = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
}
|
await MainActor.run {
|
||||||
let context = self.persistentContainer.backgroundContext
|
self.lists = sorted
|
||||||
context.perform {
|
}
|
||||||
for list in lists {
|
|
||||||
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
|
let context = persistentContainer.backgroundContext
|
||||||
existing.updateFrom(apiList: list)
|
await context.perform {
|
||||||
} else {
|
for list in lists {
|
||||||
_ = ListMO(apiList: list, context: context)
|
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
|
||||||
}
|
existing.updateFrom(apiList: list)
|
||||||
}
|
} else {
|
||||||
self.persistentContainer.save(context: context)
|
_ = ListMO(apiList: list, context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.persistentContainer.save(context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
// MainThreadBox.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/27/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MainThreadBox<T>: @unchecked Sendable {
|
||||||
|
private let _value: T
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
var value: T {
|
||||||
|
_value
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
init(value: T) {
|
||||||
|
self._value = value
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,27 +145,24 @@ class OnboardingViewController: UINavigationController {
|
||||||
throw Error.gettingAccessToken(error)
|
throw Error.gettingAccessToken(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
|
// construct a temporary Client to use to fetch the user's account
|
||||||
let tempAccountInfo = UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
|
let tempClient = Client(baseURL: instanceURL, accessToken: accessToken, session: .appDefault)
|
||||||
mastodonController.accountInfo = tempAccountInfo
|
|
||||||
|
|
||||||
updateStatus("Checking Credentials")
|
updateStatus("Checking Credentials")
|
||||||
let ownAccount: AccountMO
|
let ownAccount: Account
|
||||||
do {
|
do {
|
||||||
ownAccount = try await retrying("Getting own account") {
|
ownAccount = try await retrying("Getting own account") {
|
||||||
try await mastodonController.getOwnAccount()
|
try await tempClient.run(Client.getSelfAccount()).0
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
throw Error.gettingOwnAccount(error)
|
throw Error.gettingOwnAccount(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
|
let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
|
||||||
mastodonController.accountInfo = accountInfo
|
|
||||||
|
|
||||||
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func retrying<T>(_ label: StaticString, action: () async throws -> T) async throws -> T {
|
private func retrying<T: Sendable>(_ label: StaticString, action: () async throws -> T) async throws -> T {
|
||||||
for attempt in 0..<4 {
|
for attempt in 0..<4 {
|
||||||
do {
|
do {
|
||||||
return try await action()
|
return try await action()
|
||||||
|
|
Loading…
Reference in New Issue