mastodon-ios/Mastodon/Supporting Files/SceneDelegate.swift

350 lines
13 KiB
Swift

//
// SceneDelegate.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/1/22.
//
import UIKit
import Combine
import CoreDataStack
import MastodonCore
import MastodonExtension
import MastodonUI
import MastodonSDK
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
var window: UIWindow?
var coordinator: SceneCoordinator?
var savedShortCutItem: UIApplicationShortcutItem?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
#if DEBUG
let window = TouchesVisibleWindow(windowScene: windowScene)
self.window = window
#else
let window = UIWindow(windowScene: windowScene)
self.window = window
#endif
// set tint color
window.tintColor = UIColor.label
let appContext = AppContext.shared
let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self, appContext: appContext)
self.coordinator = sceneCoordinator
sceneCoordinator.setup()
window.makeKeyAndVisible()
if let urlContext = connectionOptions.urlContexts.first {
handleUrl(context: urlContext)
}
if let userActivity = connectionOptions.userActivities.first {
handleUniversalLink(userActivity: userActivity)
}
#if SNAPSHOT
// speedup animation
// window.layer.speed = 999
// disable animation
UIView.setAnimationsEnabled(false)
#endif
if let shortcutItem = connectionOptions.shortcutItem {
// Save it off for later when we become active.
savedShortCutItem = shortcutItem
}
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak self] defaults, _ in
guard let self = self else { return }
#if SNAPSHOT
// toggle Dark Mode
// https://stackoverflow.com/questions/32988241/how-to-access-launchenvironment-and-launcharguments-set-in-xcuiapplication-runn
if ProcessInfo.processInfo.arguments.contains("UIUserInterfaceStyleForceDark") {
self.window?.overrideUserInterfaceStyle = .dark
}
#else
self.window?.overrideUserInterfaceStyle = defaults.customUserInterfaceStyle
#endif
}
.store(in: &observations)
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
// update application badge
AppContext.shared.notificationService.applicationIconBadgeNeedsUpdate.send()
// trigger status filter update
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
// trigger authenticated user account update
AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send()
// update mutes and blocks and remove related data
AppContext.shared.instanceService.updateMutesAndBlocks()
if let shortcutItem = savedShortCutItem {
Task {
_ = await handler(shortcutItem: shortcutItem)
}
savedShortCutItem = nil
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
handleUniversalLink(userActivity: userActivity)
}
private func handleUniversalLink(userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return
}
guard let path = components.path, let authContext = coordinator?.authContext else {
return
}
let pathElements = path.split(separator: "/")
let profile: String?
if let profileInPath = pathElements[safe: 0] {
profile = String(profileInPath)
} else {
profile = nil
}
let statusID: String?
if let statusIDInPath = pathElements[safe: 1] {
statusID = String(statusIDInPath)
} else {
statusID = nil
}
switch (profile, statusID) {
case (profile, nil):
Task {
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
guard let me = authenticationBox.authentication.account() else { return }
guard let account = try await AppContext.shared.apiService.search(
query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
guard let relationship = try await AppContext.shared.apiService.relationship(
forAccounts: [account],
authenticationBox: authenticationBox
).value.first else { return }
let profileViewModel = ProfileViewModel(
context: AppContext.shared,
authContext: authContext,
account: account,
relationship: relationship,
me: me
)
_ = self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
}
case (profile, statusID):
Task {
guard let statusOnMyInstance = try await AppContext.shared.apiService.search(query: .init(q: incomingURL.absoluteString, resolve: true), authenticationBox: authContext.mastodonAuthenticationBox).value.statuses.first else { return }
let threadViewModel = RemoteThreadViewModel(
context: AppContext.shared,
authContext: authContext,
statusID: statusOnMyInstance.id
)
coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
}
case (_, _):
break
// do nothing
}
}
}
extension SceneDelegate {
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem) async -> Bool {
return await handler(shortcutItem: shortcutItem)
}
@MainActor
private func handler(shortcutItem: UIApplicationShortcutItem) async -> Bool {
switch shortcutItem.type {
case NotificationService.unreadShortcutItemIdentifier:
guard let coordinator = self.coordinator else { return false }
guard let accessToken = shortcutItem.userInfo?["accessToken"] as? String else {
assertionFailure()
return false
}
guard let authentication = AuthenticationServiceProvider.shared.getAuthentication(matching: accessToken) else {
assertionFailure()
return false
}
let _isActive = try? await coordinator.appContext.authenticationService.activeMastodonUser(
domain: authentication.domain,
userID: authentication.userID
)
guard _isActive == true else {
return false
}
coordinator.switchToTabBar(tab: .notifications)
case "org.joinmastodon.app.new-post":
showComposeViewController()
case "org.joinmastodon.app.search":
coordinator?.switchToTabBar(tab: .search)
if let searchViewController = coordinator?.tabBarController.topMost as? SearchViewController {
searchViewController.searchBarTapPublisher.send("")
}
default:
assertionFailure()
break
}
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 {
} else {
if let authContext = coordinator?.authContext {
let composeViewModel = ComposeViewModel(
context: AppContext.shared,
authContext: authContext,
composeContext: .composeStatus,
destination: .topLevel
)
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
}
}
}
private func handleUrl(context: UIOpenURLContext) {
let sendingAppID = context.options.sourceApplication
let url = context.url
if !UIApplication.shared.canOpenURL(url) { return }
#if DEBUG
print("source application = \(sendingAppID ?? "Unknown")")
print("url = \(url)")
#endif
switch url.host {
case "post":
showComposeViewController()
case "profile":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
Task {
do {
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return }
guard let account = try await AppContext.shared.apiService.search(
query: .init(q: components[1], type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
guard let relationship = try await AppContext.shared.apiService.relationship(
forAccounts: [account],
authenticationBox: authenticationBox
).value.first else { return }
let profileViewModel = ProfileViewModel(
context: AppContext.shared,
authContext: authContext,
account: account,
relationship: relationship,
me: me
)
self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
} catch {
// fail silently
}
}
case "status":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
let statusId = components[1]
// View post from user
let threadViewModel = RemoteThreadViewModel(
context: AppContext.shared,
authContext: authContext,
statusID: statusId
)
coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
case "search":
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
guard
let authContext = coordinator?.authContext,
let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value
else { return }
let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery)
coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show)
default:
return
}
}
}