Merge pull request #847 from jinsu35/fix-issue-391
Implement URL scheme
This commit is contained in:
commit
5cd9343302
|
@ -28,6 +28,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.retry(3)
|
.retry(3)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -89,4 +90,41 @@ final class RemoteProfileViewModel: ProfileViewModel {
|
||||||
} // end Task
|
} // 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ extension ThreadViewModel.LoadThreadState {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
let response = try await viewModel.context.apiService.statusContext(
|
let response = try await viewModel.context.apiService.statusContext(
|
||||||
statusID: threadContext.statusID,
|
statusID: threadContext.statusID,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import CoreDataStack
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import MastodonExtension
|
import MastodonExtension
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
#if PROFILE
|
#if PROFILE
|
||||||
import FPSIndicator
|
import FPSIndicator
|
||||||
|
@ -67,6 +68,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
sceneCoordinator.setup()
|
sceneCoordinator.setup()
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
|
||||||
|
if let urlContext = connectionOptions.urlContexts.first {
|
||||||
|
handleUrl(context: urlContext)
|
||||||
|
}
|
||||||
|
|
||||||
#if SNAPSHOT
|
#if SNAPSHOT
|
||||||
// speedup animation
|
// speedup animation
|
||||||
// window.layer.speed = 999
|
// window.layer.speed = 999
|
||||||
|
@ -187,21 +192,7 @@ extension SceneDelegate {
|
||||||
coordinator.switchToTabBar(tab: .notifications)
|
coordinator.switchToTabBar(tab: .notifications)
|
||||||
|
|
||||||
case "org.joinmastodon.app.new-post":
|
case "org.joinmastodon.app.new-post":
|
||||||
if coordinator?.tabBarController.topMost is ComposeViewController {
|
showComposeViewController()
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "org.joinmastodon.app.search":
|
case "org.joinmastodon.app.search":
|
||||||
coordinator?.switchToTabBar(tab: .search)
|
coordinator?.switchToTabBar(tab: .search)
|
||||||
|
@ -219,4 +210,75 @@ extension SceneDelegate {
|
||||||
|
|
||||||
return true
|
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)")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
|
import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import CommonOSLog
|
import CommonOSLog
|
||||||
|
@ -199,3 +200,74 @@ extension APIService {
|
||||||
return response
|
return response
|
||||||
} // end func
|
} // 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<Mastodon.Entity.Account> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,5 +50,4 @@ extension APIService {
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} // end func
|
} // end func
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue