From f71c4964f53ed6143798885aaadecc2d1ae0fce3 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Sun, 1 Jan 2023 01:00:00 -0800 Subject: [PATCH 1/5] UI update should be invoked on the main thread --- .../Profile/RemoteProfileViewModel.swift | 64 ++++++++++--------- .../ThreadViewModel+LoadThreadState.swift | 5 +- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 1e1388c95..78575e5e4 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -17,38 +17,42 @@ final class RemoteProfileViewModel: ProfileViewModel { init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { super.init(context: context, authContext: authContext, optionalMastodonUser: nil) - let domain = authContext.mastodonAuthenticationBox.domain - let authorization = authContext.mastodonAuthenticationBox.userAuthorization - Just(userID) - .asyncMap { userID in - try await context.apiService.accountInfo( - domain: domain, - userID: userID, - authorization: authorization - ) - } - .retry(3) - .sink { completion in - switch completion { - case .failure(let error): - // TODO: handle error - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, userID, error.localizedDescription) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, userID) + Task { @MainActor in + let domain = authContext.mastodonAuthenticationBox.domain + let authorization = authContext.mastodonAuthenticationBox.userAuthorization + Just(userID) + .asyncMap { userID in + try await context.apiService.accountInfo( + domain: domain, + userID: userID, + authorization: authorization + ) } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return + .retry(3) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, userID, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, userID) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let managedObjectContext = context.managedObjectContext + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) + guard let mastodonUser = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return + } + DispatchQueue.main.async { + self.user = mastodonUser + } } - self.user = mastodonUser - } - .store(in: &disposeBag) + .store(in: &disposeBag) + } } init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 1c6c40190..96ea928db 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -73,11 +73,12 @@ extension ThreadViewModel.LoadThreadState { return } - Task { + Task { @MainActor in do { let response = try await viewModel.context.apiService.statusContext( statusID: threadContext.statusID, - authenticationBox: viewModel.authContext.mastodonAuthenticationBox + authenticationBox: viewModel.authContext.mastodonAuthenticationBox, + domain: threadContext.domain ) await enter(state: NoMore.self) From de962a0c098213f6167417b169ecf706162cd940 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Sun, 1 Jan 2023 01:01:01 -0800 Subject: [PATCH 2/5] Implement URL scheme --- Mastodon/Supporting Files/SceneDelegate.swift | 106 +++++++++++++++--- .../Service/API/APIService+Account.swift | 41 +++++++ .../Service/API/APIService+Thread.swift | 80 ++++++++++++- 3 files changed, 210 insertions(+), 17 deletions(-) diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index e2f045a7b..82d4e06d7 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -12,6 +12,7 @@ import CoreDataStack import MastodonCore import MastodonExtension import MastodonUI +import MastodonSDK #if PROFILE import FPSIndicator @@ -67,6 +68,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { sceneCoordinator.setup() window.makeKeyAndVisible() + if let urlContext = connectionOptions.urlContexts.first { + handleUrl(context: urlContext) + } + #if SNAPSHOT // speedup animation // window.layer.speed = 999 @@ -187,21 +192,7 @@ extension SceneDelegate { coordinator.switchToTabBar(tab: .notifications) case "org.joinmastodon.app.new-post": - if coordinator?.tabBarController.topMost is ComposeViewController { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") - } else { - if let authContext = coordinator?.authContext { - let composeViewModel = ComposeViewModel( - context: AppContext.shared, - authContext: authContext, - destination: .topLevel - ) - _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") - } else { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") - } - } + showComposeViewController() case "org.joinmastodon.app.search": coordinator?.switchToTabBar(tab: .search) @@ -219,4 +210,89 @@ extension SceneDelegate { return true } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + // Determine who sent the URL. + if let urlContext = URLContexts.first { + handleUrl(context: urlContext) + } + } + + private func showComposeViewController() { + if coordinator?.tabBarController.topMost is ComposeViewController { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") + } else { + if let authContext = coordinator?.authContext { + let composeViewModel = ComposeViewModel( + context: AppContext.shared, + authContext: authContext, + destination: .topLevel + ) + _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") + } else { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") + } + } + } + + private func handleUrl(context: UIOpenURLContext) { + let sendingAppID = context.options.sourceApplication + let url = context.url + + if !UIApplication.shared.canOpenURL(url) { return } + + print("source application = \(sendingAppID ?? "Unknown")") + print("url = \(url)") + + if let username = url.user { + guard let host = url.host else { return } + let components = url.pathComponents + if components.count == 3 && components[1] == "status" { + let statusId = components[2] + // View post from user + print("view status \(statusId)") + if let authContext = coordinator?.authContext { + Task { + guard let thread = try await AppContext.shared.apiService.fetchThread( + statusID: statusId, + domain: host, + authenticationBox: authContext.mastodonAuthenticationBox + ) else { return } + + let threadViewModel = CachedThreadViewModel(context: AppContext.shared, + authContext: authContext, + status: thread) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } + } + } else { + print("view profile \(username)@\(host)") + if let authContext = coordinator?.authContext { + Task { @MainActor in + guard let user = try await AppContext.shared.apiService.fetchUser( + username: username, + domain: host, + authenticationBox: authContext.mastodonAuthenticationBox + ) else { return } + + let profileViewModel = RemoteProfileViewModel(context: AppContext.shared, + authContext: authContext, + userID: user.id) + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) + } + } + } + } else { + guard let action = url.host else { return } + if action == "post" { + print("make post") + showComposeViewController() + } + } + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 8f89b4b76..d68984587 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -6,6 +6,7 @@ // import os.log +import CoreDataStack import Foundation import Combine import CommonOSLog @@ -199,3 +200,43 @@ extension APIService { return response } // end func } + +extension APIService { + public func fetchUser(username: String, domain: String, authenticationBox: MastodonAuthenticationBox) + async throws -> MastodonUser? { + let query = Mastodon.API.Account.AccountLookupQuery(acct: "\(username)@\(domain)") + let authorization = authenticationBox.userAuthorization + + let response = try await Mastodon.API.Account.lookupAccount( + session: session, + domain: authenticationBox.domain, + query: query, + authorization: authorization + ).singleOutput() + + // user + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + _ = Persistence.MastodonUser.createOrMerge( + in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: response.value, + cache: nil, + networkDate: response.networkDate + ) + ) + } + var result: MastodonUser? + try await managedObjectContext.perform { + result = Persistence.MastodonUser.fetch(in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: response.value, + cache: nil, + networkDate: response.networkDate + )) + } + return result + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index ddd782856..902be16d2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -16,9 +16,10 @@ extension APIService { public func statusContext( statusID: Mastodon.Entity.Status.ID, - authenticationBox: MastodonAuthenticationBox + authenticationBox: MastodonAuthenticationBox, + domain: String? = nil ) async throws -> Mastodon.Response.Content { - let domain = authenticationBox.domain + let domain = domain ?? authenticationBox.domain let authorization = authenticationBox.userAuthorization let response = try await Mastodon.API.Statuses.statusContext( @@ -51,4 +52,79 @@ extension APIService { return response } // end func + public func fetchThread( + statusID: Mastodon.Entity.Status.ID, + domain: String, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Status? { + let authorization = authenticationBox.userAuthorization + let managedObjectContext = self.backgroundManagedObjectContext + + let responseOne = try await Mastodon.API.Statuses.status( + session: session, + domain: domain, + statusID: statusID, + authorization: authorization + ).singleOutput() + + try await managedObjectContext.performChanges { + let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + _ = Persistence.Status.createOrMerge( + in: managedObjectContext, + context: Persistence.Status.PersistContext( + domain: domain, + entity: responseOne.value, + me: me, + statusCache: nil, + userCache: nil, + networkDate: responseOne.networkDate + ) + ) + } + + let responseTwo = try await Mastodon.API.Statuses.statusContext( + session: session, + domain: domain, + statusID: statusID, + authorization: authorization + ).singleOutput() + + try await managedObjectContext.performChanges { + let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let value = responseTwo.value.ancestors + responseTwo.value.descendants + + for entity in value { + _ = Persistence.Status.createOrMerge( + in: managedObjectContext, + context: Persistence.Status.PersistContext( + domain: domain, + entity: entity, + me: me, + statusCache: nil, + userCache: nil, + networkDate: responseTwo.networkDate + ) + ) + } + } + + var result: Status? + try await managedObjectContext.perform { + let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + + if let status = Persistence.Status.fetch(in: managedObjectContext, + context: Persistence.Status.PersistContext( + domain: domain, + entity: responseOne.value, + me: me, + statusCache: nil, + userCache: nil, + networkDate: responseOne.networkDate + )) { + result = status + } + } + + return result + } } From 5c82c04232f04aed848f96d8710beef49607d7a8 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Sun, 1 Jan 2023 15:55:54 -0800 Subject: [PATCH 3/5] Use authenticated domain for API calls. Modify URL scheme --- .../Profile/RemoteProfileViewModel.swift | 66 +++++++++---------- .../ThreadViewModel+LoadThreadState.swift | 3 +- Mastodon/Supporting Files/SceneDelegate.swift | 58 ++++++++-------- .../Service/API/APIService+Thread.swift | 7 +- 4 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 78575e5e4..11a1bffcb 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -17,42 +17,40 @@ final class RemoteProfileViewModel: ProfileViewModel { init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { super.init(context: context, authContext: authContext, optionalMastodonUser: nil) - Task { @MainActor in - let domain = authContext.mastodonAuthenticationBox.domain - let authorization = authContext.mastodonAuthenticationBox.userAuthorization - Just(userID) - .asyncMap { userID in - try await context.apiService.accountInfo( - domain: domain, - userID: userID, - authorization: authorization - ) + let domain = authContext.mastodonAuthenticationBox.domain + let authorization = authContext.mastodonAuthenticationBox.userAuthorization + Just(userID) + .asyncMap { userID in + try await context.apiService.accountInfo( + domain: domain, + userID: userID, + authorization: authorization + ) + } + .retry(3) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, userID, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, userID) } - .retry(3) - .sink { completion in - switch completion { - case .failure(let error): - // TODO: handle error - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, userID, error.localizedDescription) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, userID) - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - DispatchQueue.main.async { - self.user = mastodonUser - } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let managedObjectContext = context.managedObjectContext + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) + guard let mastodonUser = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return } - .store(in: &disposeBag) - } + DispatchQueue.main.async { + self.user = mastodonUser + } + } + .store(in: &disposeBag) } init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 96ea928db..87175cf55 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -77,8 +77,7 @@ extension ThreadViewModel.LoadThreadState { do { let response = try await viewModel.context.apiService.statusContext( statusID: threadContext.statusID, - authenticationBox: viewModel.authContext.mastodonAuthenticationBox, - domain: threadContext.domain + authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) await enter(state: NoMore.self) diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 82d4e06d7..2c6e3bf50 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -245,29 +245,17 @@ extension SceneDelegate { print("source application = \(sendingAppID ?? "Unknown")") print("url = \(url)") - if let username = url.user { - guard let host = url.host else { return } + switch url.host { + case "post": + showComposeViewController() + case "profile": let components = url.pathComponents - if components.count == 3 && components[1] == "status" { - let statusId = components[2] - // View post from user - print("view status \(statusId)") - if let authContext = coordinator?.authContext { - Task { - guard let thread = try await AppContext.shared.apiService.fetchThread( - statusID: statusId, - domain: host, - authenticationBox: authContext.mastodonAuthenticationBox - ) else { return } - - let threadViewModel = CachedThreadViewModel(context: AppContext.shared, - authContext: authContext, - status: thread) - coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } - } - } else { - print("view profile \(username)@\(host)") + if components.count == 2 && components[0] == "/" { + let addr = components[1] + let tokens = addr.components(separatedBy: "@") + if tokens.count != 2 { return } + let username = tokens[0] + let host = tokens[1] if let authContext = coordinator?.authContext { Task { @MainActor in guard let user = try await AppContext.shared.apiService.fetchUser( @@ -287,12 +275,28 @@ extension SceneDelegate { } } } - } else { - guard let action = url.host else { return } - if action == "post" { - print("make post") - showComposeViewController() + case "status": + let components = url.pathComponents + if components.count == 2 && components[0] == "/" { + let statusId = components[1] + // View post from user + print("view status \(statusId)") + if let authContext = coordinator?.authContext { + Task { + guard let thread = try await AppContext.shared.apiService.fetchThread( + statusID: statusId, + authenticationBox: authContext.mastodonAuthenticationBox + ) else { return } + + let threadViewModel = CachedThreadViewModel(context: AppContext.shared, + authContext: authContext, + status: thread) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } + } } + default: + return } } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index 902be16d2..e2375e8dc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -16,10 +16,9 @@ extension APIService { public func statusContext( statusID: Mastodon.Entity.Status.ID, - authenticationBox: MastodonAuthenticationBox, - domain: String? = nil + authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let domain = domain ?? authenticationBox.domain + let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization let response = try await Mastodon.API.Statuses.statusContext( @@ -54,9 +53,9 @@ extension APIService { public func fetchThread( statusID: Mastodon.Entity.Status.ID, - domain: String, authenticationBox: MastodonAuthenticationBox ) async throws -> Status? { + let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization let managedObjectContext = self.backgroundManagedObjectContext From 843eeed616817ab218e7dab644aa2f050bceb688 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Mon, 2 Jan 2023 00:26:15 -0800 Subject: [PATCH 4/5] Leave data loading to ThreadViewModel --- .../Service/API/APIService+Thread.swift | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index e2375e8dc..d655a7e5c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -81,31 +81,31 @@ extension APIService { ) } - let responseTwo = try await Mastodon.API.Statuses.statusContext( - session: session, - domain: domain, - statusID: statusID, - authorization: authorization - ).singleOutput() - - try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user - let value = responseTwo.value.ancestors + responseTwo.value.descendants - - for entity in value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: responseTwo.networkDate - ) - ) - } - } +// let responseTwo = try await Mastodon.API.Statuses.statusContext( +// session: session, +// domain: domain, +// statusID: statusID, +// authorization: authorization +// ).singleOutput() +// +// try await managedObjectContext.performChanges { +// let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user +// let value = responseTwo.value.ancestors + responseTwo.value.descendants +// +// for entity in value { +// _ = Persistence.Status.createOrMerge( +// in: managedObjectContext, +// context: Persistence.Status.PersistContext( +// domain: domain, +// entity: entity, +// me: me, +// statusCache: nil, +// userCache: nil, +// networkDate: responseTwo.networkDate +// ) +// ) +// } +// } var result: Status? try await managedObjectContext.perform { From a00cd6007672a1ecbcb36b362d5ff5c85bd2b695 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Thu, 5 Jan 2023 15:57:58 -0800 Subject: [PATCH 5/5] Remove duplicate function fetchUser(). --- .../Profile/RemoteProfileViewModel.swift | 42 +++++++++- Mastodon/Supporting Files/SceneDelegate.swift | 46 ++++------- .../Service/API/APIService+Account.swift | 31 ++++++++ .../Service/API/APIService+Thread.swift | 76 ------------------- 4 files changed, 84 insertions(+), 111 deletions(-) diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 11a1bffcb..89ff03660 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -28,6 +28,7 @@ final class RemoteProfileViewModel: ProfileViewModel { ) } .retry(3) + .receive(on: DispatchQueue.main) .sink { completion in switch completion { case .failure(let error): @@ -46,9 +47,7 @@ final class RemoteProfileViewModel: ProfileViewModel { assertionFailure() return } - DispatchQueue.main.async { - self.user = mastodonUser - } + self.user = mastodonUser } .store(in: &disposeBag) } @@ -91,4 +90,41 @@ final class RemoteProfileViewModel: ProfileViewModel { } // end Task } + init(context: AppContext, authContext: AuthContext, acct: String) { + super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + + let domain = authContext.mastodonAuthenticationBox.domain + let authorization = authContext.mastodonAuthenticationBox.userAuthorization + Just(acct) + .asyncMap { acct in + try await context.apiService.accountSearch( + domain: domain, + query: .init(acct: acct), + authorization: authorization + ) + } + .retry(3) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, acct, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, acct) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let managedObjectContext = context.managedObjectContext + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) + guard let mastodonUser = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return + } + self.user = mastodonUser + } + .store(in: &disposeBag) + } } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 2c6e3bf50..382755c77 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -252,27 +252,17 @@ extension SceneDelegate { let components = url.pathComponents if components.count == 2 && components[0] == "/" { let addr = components[1] - let tokens = addr.components(separatedBy: "@") - if tokens.count != 2 { return } - let username = tokens[0] - let host = tokens[1] if let authContext = coordinator?.authContext { - Task { @MainActor in - guard let user = try await AppContext.shared.apiService.fetchUser( - username: username, - domain: host, - authenticationBox: authContext.mastodonAuthenticationBox - ) else { return } - - let profileViewModel = RemoteProfileViewModel(context: AppContext.shared, - authContext: authContext, - userID: user.id) - self.coordinator?.present( - scene: .profile(viewModel: profileViewModel), - from: nil, - transition: .show - ) - } + let profileViewModel = RemoteProfileViewModel( + context: AppContext.shared, + authContext: authContext, + acct: components[1] + ) + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) } } case "status": @@ -280,19 +270,11 @@ extension SceneDelegate { if components.count == 2 && components[0] == "/" { let statusId = components[1] // View post from user - print("view status \(statusId)") if let authContext = coordinator?.authContext { - Task { - guard let thread = try await AppContext.shared.apiService.fetchThread( - statusID: statusId, - authenticationBox: authContext.mastodonAuthenticationBox - ) else { return } - - let threadViewModel = CachedThreadViewModel(context: AppContext.shared, - authContext: authContext, - status: thread) - coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } + let threadViewModel = RemoteThreadViewModel(context: AppContext.shared, + authContext: authContext, + statusID: statusId) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) } } default: diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index d68984587..34d398633 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -240,3 +240,34 @@ extension APIService { return result } } + +extension APIService { + public func accountSearch( + domain: String, + query: Mastodon.API.Account.AccountLookupQuery, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let response = try await Mastodon.API.Account.lookupAccount( + session: session, + domain: domain, + query: query, + authorization: authorization + ).singleOutput() + + // user + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + _ = Persistence.MastodonUser.createOrMerge( + in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: domain, + entity: response.value, + cache: nil, + networkDate: response.networkDate + ) + ) + } + + return response + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index d655a7e5c..f1b94376e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -50,80 +50,4 @@ extension APIService { return response } // end func - - public func fetchThread( - statusID: Mastodon.Entity.Status.ID, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Status? { - let domain = authenticationBox.domain - let authorization = authenticationBox.userAuthorization - let managedObjectContext = self.backgroundManagedObjectContext - - let responseOne = try await Mastodon.API.Statuses.status( - session: session, - domain: domain, - statusID: statusID, - authorization: authorization - ).singleOutput() - - try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: responseOne.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: responseOne.networkDate - ) - ) - } - -// let responseTwo = try await Mastodon.API.Statuses.statusContext( -// session: session, -// domain: domain, -// statusID: statusID, -// authorization: authorization -// ).singleOutput() -// -// try await managedObjectContext.performChanges { -// let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user -// let value = responseTwo.value.ancestors + responseTwo.value.descendants -// -// for entity in value { -// _ = Persistence.Status.createOrMerge( -// in: managedObjectContext, -// context: Persistence.Status.PersistContext( -// domain: domain, -// entity: entity, -// me: me, -// statusCache: nil, -// userCache: nil, -// networkDate: responseTwo.networkDate -// ) -// ) -// } -// } - - var result: Status? - try await managedObjectContext.perform { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user - - if let status = Persistence.Status.fetch(in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: responseOne.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: responseOne.networkDate - )) { - result = status - } - } - - return result - } }