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 }
|
||||
|
||||
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
|
||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void)
|
||||
|
||||
func getCustomEmojis() async -> [Emoji]
|
||||
|
||||
@MainActor
|
||||
func searchCachedAccounts(query: String) -> [AccountProtocol]
|
||||
|
|
|
@ -44,11 +44,7 @@ class AutocompleteEmojisController: ViewController {
|
|||
|
||||
@MainActor
|
||||
private func queryChanged(_ query: String) async {
|
||||
var emojis = await withCheckedContinuation { continuation in
|
||||
composeController.mastodonController.getCustomEmojis {
|
||||
continuation.resume(returning: $0)
|
||||
}
|
||||
}
|
||||
var emojis = await composeController.mastodonController.getCustomEmojis()
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import WebURL
|
|||
/**
|
||||
The base Mastodon API client.
|
||||
*/
|
||||
public class Client {
|
||||
public struct Client: Sendable {
|
||||
|
||||
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
|
||||
|
||||
|
@ -20,8 +20,6 @@ public class Client {
|
|||
let session: URLSession
|
||||
|
||||
public var accessToken: String?
|
||||
|
||||
public var appID: String?
|
||||
public var clientID: String?
|
||||
public var clientSecret: String?
|
||||
|
||||
|
@ -61,9 +59,11 @@ public class Client {
|
|||
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.accessToken = accessToken
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
self.session = session
|
||||
}
|
||||
|
||||
|
@ -150,14 +150,7 @@ public class Client {
|
|||
"scopes" => scopes.scopeString,
|
||||
"website" => website?.absoluteString
|
||||
]))
|
||||
run(request) { result in
|
||||
defer { completion(result) }
|
||||
guard case let .success(application, _) = result else { return }
|
||||
|
||||
self.appID = application.id
|
||||
self.clientID = application.clientID
|
||||
self.clientSecret = application.clientSecret
|
||||
}
|
||||
run(request, completion: completion)
|
||||
}
|
||||
|
||||
public func getAccessToken(authorizationCode: String, redirectURI: String, scopes: [Scope], completion: @escaping Callback<LoginSettings>) {
|
||||
|
@ -169,12 +162,7 @@ public class Client {
|
|||
"redirect_uri" => redirectURI,
|
||||
"scope" => scopes.scopeString,
|
||||
]))
|
||||
run(request) { result in
|
||||
defer { completion(result) }
|
||||
guard case let .success(loginSettings, _) = result else { return }
|
||||
|
||||
self.accessToken = loginSettings.accessToken
|
||||
}
|
||||
run(request, completion: completion)
|
||||
}
|
||||
|
||||
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")
|
||||
run(wellKnown) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
completion(.failure(error))
|
||||
|
||||
case let .success(wellKnown, _):
|
||||
if let url = wellKnown.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
||||
let wellKnownResults = try await run(wellKnown).0
|
||||
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
||||
let href = WebURL(url.href),
|
||||
href.host == WebURL(self.baseURL)?.host {
|
||||
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
||||
self.run(nodeInfo, completion: completion)
|
||||
}
|
||||
}
|
||||
return try await run(nodeInfo).0
|
||||
} else {
|
||||
throw NodeInfoError.noWellKnownLink
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,4 +583,15 @@ extension Client {
|
|||
case invalidModel(Swift.Error)
|
||||
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
|
||||
async let instance = try? await run(Client.getInstanceV1()).0
|
||||
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
|
||||
self.client.nodeInfo { response in
|
||||
switch response {
|
||||
case .success(let nodeInfo, _):
|
||||
continuation.resume(returning: nodeInfo)
|
||||
case .failure(_):
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
async let nodeInfo = try? await client.nodeInfo()
|
||||
guard let instance = await instance else { return }
|
||||
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
|
||||
}
|
||||
|
@ -66,15 +57,13 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
|
||||
func getCustomEmojis() async -> [Emoji] {
|
||||
if let customEmojis {
|
||||
completion(customEmojis)
|
||||
return customEmojis
|
||||
} else {
|
||||
Task.detached { @MainActor in
|
||||
let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? []
|
||||
self.customEmojis = emojis
|
||||
completion(emojis)
|
||||
}
|
||||
return emojis
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -314,6 +314,7 @@
|
|||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.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 */; };
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1502,6 +1504,7 @@
|
|||
D61F75BC293D099600C0B37F /* Lazy.swift */,
|
||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||
D61DC84528F498F200B82C6E /* Logging.swift */,
|
||||
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
|
||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||
|
@ -2149,6 +2152,7 @@
|
|||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */,
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */,
|
||||
D6DEBA8D2B6579830008629A /* MainThreadBox.swift in Sources */,
|
||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||
|
|
|
@ -15,8 +15,10 @@ import InstanceFeatures
|
|||
import Sentry
|
||||
#endif
|
||||
import ComposeUI
|
||||
import OSLog
|
||||
|
||||
private let oauthScopes = [Scope.read, .write, .follow]
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
||||
|
||||
class MastodonController: ObservableObject {
|
||||
|
||||
|
@ -48,26 +50,30 @@ class MastodonController: ObservableObject {
|
|||
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
||||
|
||||
let instanceURL: URL
|
||||
var accountInfo: UserAccountInfo?
|
||||
var accountPreferences: AccountPreferences!
|
||||
let accountInfo: UserAccountInfo?
|
||||
private(set) var accountPreferences: AccountPreferences!
|
||||
|
||||
let client: Client!
|
||||
private(set) var client: Client!
|
||||
let instanceFeatures = InstanceFeatures()
|
||||
|
||||
@Published private(set) var account: AccountMO?
|
||||
@Published private(set) var instance: InstanceV1?
|
||||
@Published private var instanceV2: InstanceV2?
|
||||
@Published private(set) var instanceInfo: InstanceInfo!
|
||||
@Published private(set) var nodeInfo: NodeInfo!
|
||||
@Published private(set) var lists: [List] = []
|
||||
@Published private(set) var customEmojis: [Emoji]?
|
||||
@Published private(set) var followedHashtags: [FollowedHashtag] = []
|
||||
@Published private(set) var filters: [FilterMO] = []
|
||||
@MainActor @Published private(set) var account: AccountMO?
|
||||
@MainActor @Published private(set) var instance: InstanceV1?
|
||||
@MainActor @Published private var instanceV2: InstanceV2?
|
||||
@MainActor @Published private(set) var instanceInfo: InstanceInfo!
|
||||
@MainActor @Published private(set) var nodeInfo: NodeInfo!
|
||||
@MainActor @Published private(set) var lists: [List] = []
|
||||
@MainActor @Published private(set) var customEmojis: [Emoji]?
|
||||
@MainActor @Published private(set) var followedHashtags: [FollowedHashtag] = []
|
||||
@MainActor @Published private(set) var filters: [FilterMO] = []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
|
||||
private var ownInstanceRequest: URLSessionTask?
|
||||
@MainActor
|
||||
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 {
|
||||
accountInfo != nil
|
||||
|
@ -222,22 +228,37 @@ class MastodonController: ObservableObject {
|
|||
|
||||
Task {
|
||||
do {
|
||||
async let ownAccount = try 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()
|
||||
_ = try await getOwnAccount()
|
||||
} 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
|
||||
|
@ -250,19 +271,29 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
|
||||
if let account {
|
||||
func getOwnAccount(completion: (@Sendable (Result<AccountMO, any Error>) -> Void)? = nil) {
|
||||
Task.detached {
|
||||
do {
|
||||
let account = try await self.getOwnAccount()
|
||||
completion?(.success(account))
|
||||
} else {
|
||||
let request = Client.getSelfAccount()
|
||||
run(request) { response in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
} catch {
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case let .success(account, _):
|
||||
let context = self.persistentContainer.viewContext
|
||||
context.perform {
|
||||
@MainActor
|
||||
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
|
||||
|
@ -270,16 +301,13 @@ class MastodonController: ObservableObject {
|
|||
} else {
|
||||
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
|
||||
}
|
||||
// TODO: is AccountMO.active used anywhere?
|
||||
accountMO.active = true
|
||||
self.account = accountMO
|
||||
completion?(.success(accountMO))
|
||||
return MainThreadBox(value: accountMO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getOwnAccount() async throws -> AccountMO {
|
||||
if let account = account {
|
||||
return account
|
||||
} else {
|
||||
|
@ -291,90 +319,60 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
|
||||
getOwnInstanceInternal(retryAttempt: 0) {
|
||||
if case let .success(instance) = $0 {
|
||||
completion?(instance)
|
||||
func getOwnInstance(completion: (@Sendable (InstanceV1) -> Void)? = nil) {
|
||||
Task.detached {
|
||||
let ownInstance = try? await self.getOwnInstance()
|
||||
if let ownInstance {
|
||||
completion?(ownInstance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func getOwnInstance() async throws -> InstanceV1 {
|
||||
return try await withCheckedThrowingContinuation({ continuation in
|
||||
getOwnInstanceInternal(retryAttempt: 0) { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
if let instance {
|
||||
return instance
|
||||
} else if let fetchOwnInstanceTask {
|
||||
return try await fetchOwnInstanceTask.value
|
||||
} else {
|
||||
if let completion = completion {
|
||||
pendingOwnInstanceRequestCallbacks.append(completion)
|
||||
let task = Task {
|
||||
let instance = try await retrying("Fetch Own Instance") {
|
||||
try await run(Client.getInstanceV1()).0
|
||||
}
|
||||
|
||||
if ownInstanceRequest == nil {
|
||||
let request = Client.getInstanceV1()
|
||||
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))
|
||||
Task {
|
||||
await fetchNodeInfo()
|
||||
}
|
||||
self.pendingOwnInstanceRequestCallbacks = []
|
||||
|
||||
return instance
|
||||
}
|
||||
fetchOwnInstanceTask = task
|
||||
return try await task.value
|
||||
}
|
||||
}
|
||||
|
||||
client.nodeInfo { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
print("Unable to get node info: \(error)")
|
||||
|
||||
case let .success(nodeInfo, _):
|
||||
DispatchQueue.main.async {
|
||||
@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 {
|
||||
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
|
||||
}
|
||||
|
@ -425,33 +423,35 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
|
||||
if let emojis = self.customEmojis {
|
||||
completion(emojis)
|
||||
@MainActor
|
||||
func getCustomEmojis() async -> [Emoji] {
|
||||
if let customEmojis {
|
||||
return customEmojis
|
||||
} else if let fetchCustomEmojisTask {
|
||||
return await fetchCustomEmojisTask.value
|
||||
} else {
|
||||
let request = Client.getCustomEmoji()
|
||||
run(request) { (response) in
|
||||
if case let .success(emojis, _) = response {
|
||||
DispatchQueue.main.async {
|
||||
self.customEmojis = emojis
|
||||
}
|
||||
completion(emojis)
|
||||
} else {
|
||||
completion([])
|
||||
}
|
||||
let task = Task {
|
||||
let emojis = (try? await run(Client.getCustomEmoji()).0) ?? []
|
||||
customEmojis = emojis
|
||||
return emojis
|
||||
}
|
||||
fetchCustomEmojisTask = task
|
||||
return await task.value
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLists() {
|
||||
private func loadLists() async {
|
||||
let req = Client.getLists()
|
||||
run(req) { response in
|
||||
if case .success(let lists, _) = response {
|
||||
DispatchQueue.main.async {
|
||||
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||
guard let (lists, _) = try? await run(req) else {
|
||||
return
|
||||
}
|
||||
let context = self.persistentContainer.backgroundContext
|
||||
context.perform {
|
||||
let sorted = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||
await MainActor.run {
|
||||
self.lists = sorted
|
||||
}
|
||||
|
||||
let context = persistentContainer.backgroundContext
|
||||
await context.perform {
|
||||
for list in lists {
|
||||
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
|
||||
existing.updateFrom(apiList: list)
|
||||
|
@ -462,8 +462,6 @@ class MastodonController: ObservableObject {
|
|||
self.persistentContainer.save(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadCachedLists() -> [List] {
|
||||
let req = ListMO.fetchRequest()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
|
||||
let tempAccountInfo = UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
|
||||
mastodonController.accountInfo = tempAccountInfo
|
||||
// construct a temporary Client to use to fetch the user's account
|
||||
let tempClient = Client(baseURL: instanceURL, accessToken: accessToken, session: .appDefault)
|
||||
|
||||
updateStatus("Checking Credentials")
|
||||
let ownAccount: AccountMO
|
||||
let ownAccount: Account
|
||||
do {
|
||||
ownAccount = try await retrying("Getting own account") {
|
||||
try await mastodonController.getOwnAccount()
|
||||
try await tempClient.run(Client.getSelfAccount()).0
|
||||
}
|
||||
} catch {
|
||||
throw Error.gettingOwnAccount(error)
|
||||
}
|
||||
|
||||
let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
|
||||
mastodonController.accountInfo = 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 {
|
||||
do {
|
||||
return try await action()
|
||||
|
|
Loading…
Reference in New Issue