forked from shadowfacts/Tusker
293 lines
13 KiB
Swift
293 lines
13 KiB
Swift
//
|
|
// MainSceneDelegate.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 1/6/20.
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import MessageUI
|
|
import CoreData
|
|
#if canImport(Duckable)
|
|
import Duckable
|
|
#endif
|
|
import UserAccounts
|
|
import ComposeUI
|
|
|
|
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
|
|
|
var window: UIWindow?
|
|
|
|
private var launchActivity: NSUserActivity?
|
|
|
|
var rootViewController: TuskerRootViewController? {
|
|
window?.rootViewController as? TuskerRootViewController
|
|
}
|
|
|
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
guard let windowScene = scene as? UIWindowScene else { return }
|
|
|
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
|
launchActivity = activity
|
|
}
|
|
stateRestorationLogger.info("MainSceneDelegate.launchActivity = \(self.launchActivity?.activityType ?? "nil", privacy: .public)")
|
|
|
|
window = UIWindow(windowScene: windowScene)
|
|
|
|
showAppOrOnboardingUI(session: session)
|
|
if !connectionOptions.urlContexts.isEmpty {
|
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
|
}
|
|
|
|
window!.makeKeyAndVisible()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
|
themePrefChanged()
|
|
|
|
if let shortcutItem = connectionOptions.shortcutItem {
|
|
_ = AppShortcutItem.handle(shortcutItem)
|
|
}
|
|
}
|
|
|
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
guard let url = URLContexts.first?.url,
|
|
var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
let rootViewController else {
|
|
return
|
|
}
|
|
|
|
if components.host == "compose" {
|
|
if let mastodonController = window!.windowScene!.session.mastodonController {
|
|
let draft = mastodonController.createDraft()
|
|
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
|
draft.text = text ?? ""
|
|
rootViewController.compose(editing: draft, animated: true, isDucked: false)
|
|
}
|
|
} else {
|
|
// Assume anything else is a search query
|
|
components.scheme = "https"
|
|
let query = components.string!
|
|
rootViewController.performSearch(query: query)
|
|
}
|
|
}
|
|
|
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
|
|
let context: any UserActivityHandlingContext
|
|
if let account = UserActivityManager.getAccount(from: userActivity),
|
|
account.id != scene.session.mastodonController!.accountInfo!.id {
|
|
stateRestorationLogger.info("MainSceneDelegate cannot resume user activity for different account")
|
|
return
|
|
} else {
|
|
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
|
}
|
|
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
|
}
|
|
|
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
|
completionHandler(AppShortcutItem.handle(shortcutItem))
|
|
}
|
|
|
|
func sceneDidDisconnect(_ scene: UIScene) {
|
|
// Called as the scene is being released by the system.
|
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
|
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
|
|
|
|
Preferences.save()
|
|
DraftsPersistentContainer.shared.save()
|
|
}
|
|
|
|
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.
|
|
}
|
|
|
|
func sceneWillResignActive(_ scene: UIScene) {
|
|
// Called when the scene will move from an active state to an inactive state.
|
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
|
|
|
Preferences.save()
|
|
DraftsPersistentContainer.shared.save()
|
|
}
|
|
|
|
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
|
if let mastodonController = window?.windowScene?.session.mastodonController {
|
|
if let vcActivity = rootViewController?.stateRestorationActivity() {
|
|
vcActivity.isStateRestorationActivity = true
|
|
stateRestorationLogger.info("MainSceneDelegate returning stateRestorationActivity of type \(vcActivity.activityType, privacy: .public) from VC")
|
|
return vcActivity
|
|
} else {
|
|
// need to have an activity to make sure the same account is used
|
|
return UserActivityManager.mainSceneActivity(accountID: mastodonController.accountInfo!.id)
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
|
// Called as the scene transitions from the background to the foreground.
|
|
// Use this method to undo the changes made on entering the background.
|
|
}
|
|
|
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
// Called as the scene transitions from the foreground to the background.
|
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
|
// to restore the scene back to its current state.
|
|
|
|
if let rootVC = window?.rootViewController as? BackgroundableViewController {
|
|
rootVC.sceneDidEnterBackground()
|
|
}
|
|
|
|
if let context = scene.session.mastodonController?.persistentContainer.viewContext,
|
|
// if the user quickly opens and then closes the app, this may race with loading the persistent store, so in that event we skip cleanup/save
|
|
let psc = context.persistentStoreCoordinator,
|
|
!psc.persistentStores.isEmpty {
|
|
var minDate = Date()
|
|
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
|
|
|
let statusReq: NSFetchRequest<NSFetchRequestResult> = StatusMO.fetchRequest()
|
|
statusReq.predicate = NSPredicate(format: "((lastFetchedAt = nil) OR (lastFetchedAt < %@)) AND (reblogs.@count = 0)", minDate as NSDate)
|
|
let deleteStatusReq = NSBatchDeleteRequest(fetchRequest: statusReq)
|
|
deleteStatusReq.resultType = .resultTypeCount
|
|
if let res = try? context.execute(deleteStatusReq) as? NSBatchDeleteResult {
|
|
Logging.general.info("Pruned \(res.result as! Int) statuses")
|
|
}
|
|
|
|
|
|
let accountReq: NSFetchRequest<NSFetchRequestResult> = AccountMO.fetchRequest()
|
|
accountReq.predicate = NSPredicate(format: "((lastFetchedAt = nil) OR (lastFetchedAt < %@)) AND (statuses.@count = 0)", minDate as NSDate)
|
|
let deleteAccountReq = NSBatchDeleteRequest(fetchRequest: accountReq)
|
|
deleteAccountReq.resultType = .resultTypeCount
|
|
if let res = try? context.execute(deleteAccountReq) as? NSBatchDeleteResult {
|
|
Logging.general.info("Pruned \(res.result as! Int) accounts")
|
|
}
|
|
|
|
try? context.save()
|
|
}
|
|
|
|
BackgroundManager.shared.scheduleTasks()
|
|
}
|
|
|
|
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
|
let session = session ?? window!.windowScene!.session
|
|
if UserAccountsManager.shared.onboardingComplete {
|
|
let account: UserAccountInfo
|
|
if let activity = launchActivity,
|
|
let activityAccount = UserActivityManager.getAccount(from: activity) {
|
|
account = activityAccount
|
|
} else {
|
|
account = UserAccountsManager.shared.getMostRecentAccount()!
|
|
}
|
|
|
|
if session.mastodonController == nil {
|
|
session.mastodonController = MastodonController.getForAccount(account)
|
|
}
|
|
|
|
activateAccount(account, animated: false)
|
|
|
|
if let activity = launchActivity {
|
|
func doRestoreActivity(context: UserActivityHandlingContext) {
|
|
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
|
context.finalize(activity: activity)
|
|
}
|
|
if activity.isStateRestorationActivity {
|
|
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
|
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
|
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(isHandoff: false, root: rootViewController!))
|
|
} else {
|
|
stateRestorationLogger.fault("MainSceneDelegate launched with non-restorable activity \(activity.activityType, privacy: .public)")
|
|
}
|
|
}
|
|
} else {
|
|
window!.rootViewController = createOnboardingUI()
|
|
}
|
|
}
|
|
|
|
func activateAccount(_ account: UserAccountInfo, animated: Bool) {
|
|
let oldMostRecentAccount = UserAccountsManager.shared.mostRecentAccountID
|
|
UserAccountsManager.shared.setMostRecentAccount(account)
|
|
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
|
|
|
// iPadOS shows the title below the App Name
|
|
// macOS uses the title as the window title, but uses the subtitle in addition to the default window title (the app name)
|
|
// so this way we get basically the same behavior on both
|
|
if ProcessInfo.processInfo.isiOSAppOnMac || ProcessInfo.processInfo.isMacCatalystApp {
|
|
window!.windowScene!.subtitle = account.instanceURL.host!
|
|
} else {
|
|
window!.windowScene!.title = account.instanceURL.host!
|
|
}
|
|
|
|
let newRoot = createAppUI()
|
|
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
|
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
|
if animated,
|
|
let oldIndex = UserAccountsManager.shared.accounts.firstIndex(where: { $0.id == oldMostRecentAccount }),
|
|
let newIndex = UserAccountsManager.shared.accounts.firstIndex(of: account) {
|
|
direction = newIndex > oldIndex ? .upwards : .downwards
|
|
} else {
|
|
direction = .none
|
|
}
|
|
container.setRoot(newRoot, for: account, animating: direction)
|
|
} else {
|
|
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot, for: account)
|
|
}
|
|
}
|
|
|
|
func logoutCurrent() {
|
|
guard let account = window?.windowScene?.session.mastodonController?.accountInfo else {
|
|
return
|
|
}
|
|
LogoutService(accountInfo: account).run()
|
|
if UserAccountsManager.shared.onboardingComplete {
|
|
activateAccount(UserAccountsManager.shared.accounts.first!, animated: false)
|
|
} else {
|
|
window!.rootViewController = createOnboardingUI()
|
|
}
|
|
}
|
|
|
|
func createAppUI() -> AccountSwitchableViewController {
|
|
let mastodonController = window!.windowScene!.session.mastodonController!
|
|
mastodonController.initialize()
|
|
|
|
#if os(visionOS)
|
|
return MainTabBarViewController(mastodonController: mastodonController)
|
|
#else
|
|
let split = MainSplitViewController(mastodonController: mastodonController)
|
|
if UIDevice.current.userInterfaceIdiom == .phone,
|
|
#available(iOS 16.0, *) {
|
|
// TODO: maybe the duckable container should be outside the account switching container
|
|
return DuckableContainerViewController(child: split)
|
|
} else {
|
|
return split
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func createOnboardingUI() -> UIViewController {
|
|
let onboarding = OnboardingViewController()
|
|
onboarding.onboardingDelegate = self
|
|
return onboarding
|
|
}
|
|
|
|
@objc func themePrefChanged() {
|
|
applyAppearancePreferences()
|
|
}
|
|
|
|
func showAddAccount() {
|
|
rootViewController?.presentPreferences {
|
|
NotificationCenter.default.post(name: .addAccount, object: nil)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
|
func didFinishOnboarding(account: UserAccountInfo) {
|
|
activateAccount(account, animated: false)
|
|
}
|
|
}
|