Implement URL scheme

This commit is contained in:
jinsu kim 2023-01-01 01:01:01 -08:00
parent f71c4964f5
commit de962a0c09
3 changed files with 210 additions and 17 deletions

View File

@ -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<UIOpenURLContext>) {
// 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()
}
}
}
}

View File

@ -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
}
}

View File

@ -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<Mastodon.Entity.Context> {
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
}
}