diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 1e1388c95..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): @@ -89,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/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 1c6c40190..87175cf55 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -73,7 +73,7 @@ extension ThreadViewModel.LoadThreadState { return } - Task { + Task { @MainActor in do { let response = try await viewModel.context.apiService.statusContext( statusID: threadContext.statusID, diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index e2f045a7b..382755c77 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,75 @@ 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)") + + switch url.host { + case "post": + showComposeViewController() + case "profile": + let components = url.pathComponents + if components.count == 2 && components[0] == "/" { + let addr = components[1] + if let authContext = coordinator?.authContext { + let profileViewModel = RemoteProfileViewModel( + context: AppContext.shared, + authContext: authContext, + acct: components[1] + ) + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) + } + } + case "status": + let components = url.pathComponents + if components.count == 2 && components[0] == "/" { + let statusId = components[1] + // View post from user + if let authContext = coordinator?.authContext { + let threadViewModel = RemoteThreadViewModel(context: AppContext.shared, + authContext: authContext, + statusID: statusId) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } + } + default: + return + } + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 8f89b4b76..34d398633 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,74 @@ 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 + } +} + +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 ddd782856..f1b94376e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -50,5 +50,4 @@ extension APIService { return response } // end func - }