Merge pull request #847 from jinsu35/fix-issue-391

Implement URL scheme
This commit is contained in:
Marcus Kida 2023-01-06 10:31:06 +01:00 committed by GitHub
commit 5cd9343302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -50,5 +50,4 @@ extension APIService {
return response return response
} // end func } // end func
} }