From de962a0c098213f6167417b169ecf706162cd940 Mon Sep 17 00:00:00 2001 From: jinsu kim Date: Sun, 1 Jan 2023 01:01:01 -0800 Subject: [PATCH] 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 + } }