Compare commits

...

3 Commits

Author SHA1 Message Date
Shadowfacts cd8f0e7926 Use navigation sequencing for user activity handling 2024-08-22 14:49:27 -04:00
Shadowfacts 960ba84683 New way of sequencing navigation operations
Better fix for #484
2024-08-22 14:34:05 -04:00
Shadowfacts 2eead1f9de Revert "Fix crash when opening push notification while VC modally presented"
This reverts commit 0f2a85b108.

This fixes state restoration happening asynchronously and causing the
new tab bar animation to run.
2024-08-22 14:17:04 -04:00
10 changed files with 211 additions and 127 deletions

View File

@ -292,12 +292,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let rootViewController = delegate.rootViewController { let rootViewController = delegate.rootViewController {
let mastodonController = MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
// if the scene is already active, then we animate the account switching if necessary // if the scene is already active, then we animate things
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) let animated = scene.activationState == .foregroundActive
rootViewController.select(route: .notifications, animated: false) { delegate.activateAccount(account, animated: animated)
rootViewController.runNavigation(animated: animated) { navigation in
navigation.select(route: .notifications)
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
rootViewController.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
} }
} else { } else {
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)

View File

@ -83,9 +83,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else { } else {
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!) context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
} }
Task(priority: .userInitiated) { _ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
_ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
}
} }
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
@ -193,11 +191,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
if let activity = launchActivity { if let activity = launchActivity {
func doRestoreActivity(context: UserActivityHandlingContext) { func doRestoreActivity(context: UserActivityHandlingContext) {
Task(priority: .userInitiated) { _ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
_ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
context.finalize(activity: activity) context.finalize(activity: activity)
} }
}
if activity.isStateRestorationActivity { if activity.isStateRestorationActivity {
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!)) doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
} else if activity.activityType != UserActivityType.mainScene.rawValue { } else if activity.activityType != UserActivityType.mainScene.rawValue {

View File

@ -79,13 +79,11 @@ class AccountSwitchingContainerViewController: UIViewController {
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)") stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
} else { } else {
newRoot = newRootProvider() newRoot = newRootProvider()
Task(priority: .userInitiated) {
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
let context = StateRestorationUserActivityHandlingContext(root: newRoot) let context = StateRestorationUserActivityHandlingContext(root: newRoot)
_ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) _ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
context.finalize(activity: activity) context.finalize(activity: activity)
} }
}
} else { } else {
newRoot = newRootProvider() newRoot = newRootProvider()
} }

View File

@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
case let .tab(tab): case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly // sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false) tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController

View File

@ -54,19 +54,22 @@ class MainTabBarViewController: BaseMainTabBarViewController {
view.backgroundColor = .appBackground view.backgroundColor = .appBackground
} }
func select(tab: Tab, dismissPresented: Bool) { func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
if tab == .compose { if tab == .compose {
compose(editing: nil) compose(editing: nil, completion: completion)
} else { } else {
// when switching tabs, dismiss the currently presented VC // when switching tabs, dismiss the currently presented VC
// otherwise the selected tab changes behind the presented VC // otherwise the selected tab changes behind the presented VC
if presentedViewController != nil && dismissPresented { if presentedViewController != nil && dismissPresented {
dismiss(animated: true) { dismiss(animated: animated) {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
self.selectedIndex = tab.rawValue self.selectedIndex = tab.rawValue
completion?()
} }
} else { } else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)") stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue selectedIndex = tab.rawValue
completion?()
} }
} }
} }
@ -151,25 +154,24 @@ extension MainTabBarViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
switch route { switch route {
case .timelines: case .timelines:
select(tab: .timelines, dismissPresented: true) select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
case .notifications: case .notifications:
select(tab: .notifications, dismissPresented: true) select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
case .myProfile: case .myProfile:
select(tab: .myProfile, dismissPresented: true) select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
case .explore: case .explore:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
case .bookmarks: case .bookmarks:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated) getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
case .list(id: let id): case .list(id: let id):
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
if let list = mastodonController.getCachedList(id: id) { if let list = mastodonController.getCachedList(id: id) {
let nav = getNavigationController() let nav = getNavigationController()
_ = nav.popToRootViewController(animated: animated) _ = nav.popToRootViewController(animated: animated)
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated) nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
} }
} }
completion?()
} }
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {
@ -186,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController {
return return
} }
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: false)
exploreNavController.popToRootViewController(animated: false) exploreNavController.popToRootViewController(animated: false)
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time // setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time

View File

@ -20,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
} }
extension TuskerRootViewController {
func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) {
let sequence = TuskerNavigationSequence(root: self, animated: animated)
builder(sequence)
sequence.run()
}
}
enum TuskerRoute { enum TuskerRoute {
case timelines case timelines
case notifications case notifications
@ -29,6 +37,67 @@ enum TuskerRoute {
case list(id: String) case list(id: String)
} }
/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``.
///
/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes.
@MainActor
final class TuskerNavigationSequence {
private let root: any TuskerRootViewController
private let animated: Bool
private var operations = [() -> Void]()
init(root: any TuskerRootViewController, animated: Bool) {
self.root = root
self.animated = animated
}
func select(route: TuskerRoute) {
operations.append {
self.root.select(route: route, animated: self.animated, completion: self.run)
}
}
func push(viewController: UIViewController) {
operations.append {
let nav = self.root.getNavigationController()
nav.pushViewController(viewController, animated: self.animated)
self.run()
}
}
func popToRoot() {
operations.append {
let nav = self.root.getNavigationController()
nav.popToRootViewController(animated: self.animated)
self.run()
}
}
func present(viewController: UIViewController) {
operations.append {
self.root.present(viewController, animated: self.animated, completion: self.run)
}
}
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
operations.append {
block(self.root.getNavigationController().topViewController, self.run)
}
}
func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) {
operations.append {
operation(self.run)
}
}
func run() {
if !operations.isEmpty {
operations.removeFirst()()
}
}
}
@MainActor @MainActor
protocol NavigationControllerProtocol: UIViewController { protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set } var viewControllers: [UIViewController] { get set }

View File

@ -43,9 +43,9 @@ extension NSUserActivity {
} }
@MainActor @MainActor
func handleResume(manager: UserActivityManager) async -> Bool { func handleResume(manager: UserActivityManager) -> Bool {
guard let type = UserActivityType(rawValue: activityType) else { return false } guard let type = UserActivityType(rawValue: activityType) else { return false }
await type.handle(manager)(self) type.handle(manager)(self)
return true return true
} }

View File

@ -16,93 +16,94 @@ import ComposeUI
protocol UserActivityHandlingContext { protocol UserActivityHandlingContext {
var isHandoff: Bool { get } var isHandoff: Bool { get }
func select(route: TuskerRoute) async func select(route: TuskerRoute)
func select(route: TuskerRoute, completion: (() -> Void)?)
func present(_ vc: UIViewController)
var topViewController: UIViewController? { get }
func popToRoot() func popToRoot()
func push(_ vc: UIViewController) func push(_ vc: UIViewController)
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void)
func present(_ vc: UIViewController)
func compose(editing draft: Draft) func compose(editing draft: Draft)
func finalize(activity: NSUserActivity) func finalize(activity: NSUserActivity)
} }
extension UserActivityHandlingContext {
func select(route: TuskerRoute) async {
await withCheckedContinuation { continuation in
select(route: route) {
continuation.resume()
}
}
}
}
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
let isHandoff: Bool let isHandoff: Bool
let root: TuskerRootViewController private let root: TuskerRootViewController
var navigationDelegate: TuskerNavigationDelegate { private let navigation: TuskerNavigationSequence
root.getNavigationDelegate()!
init(isHandoff: Bool, root: TuskerRootViewController) {
self.isHandoff = isHandoff
self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: true)
} }
func select(route: TuskerRoute, completion: (() -> Void)?) { func select(route: TuskerRoute) {
root.select(route: route, animated: true, completion: completion) navigation.select(route: route)
} }
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
navigationDelegate.present(vc, animated: true) navigation.present(viewController: vc)
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController }
func popToRoot() { func popToRoot() {
_ = root.getNavigationController().popToRootViewController(animated: true) navigation.popToRoot()
} }
func push(_ vc: UIViewController) { func push(_ vc: UIViewController) {
navigationDelegate.show(vc, sender: nil) navigation.push(viewController: vc)
}
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
navigation.withTopViewController(block)
} }
func compose(editing draft: Draft) { func compose(editing draft: Draft) {
navigationDelegate.compose(editing: draft, animated: true, isDucked: true) navigation.addOperation { completion in
root.compose(editing: draft, animated: true, isDucked: true, completion: completion)
}
} }
func finalize(activity: NSUserActivity) { func finalize(activity: NSUserActivity) {
navigation.run()
} }
} }
class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
private var state = State.initial private var state = State.initial
let root: TuskerRootViewController private let root: TuskerRootViewController
private let navigation: TuskerNavigationSequence
init(root: TuskerRootViewController) { init(root: TuskerRootViewController) {
self.root = root self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: false)
} }
var isHandoff: Bool { false } var isHandoff: Bool {
false
func select(route: TuskerRoute, completion: (() -> Void)?) {
root.select(route: route, animated: false) {
self.state = .selectedRoute
completion?()
}
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController } func select(route: TuskerRoute) {
navigation.select(route: route)
state = .selectedRoute
}
func popToRoot() { func popToRoot() {
// unnecessary during state restoration navigation.popToRoot()
} }
func push(_ vc: UIViewController) { func push(_ vc: UIViewController) {
precondition(state >= .selectedRoute) precondition(state >= .selectedRoute)
root.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
state = .pushed state = .pushed
} }
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
navigation.withTopViewController(block)
}
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
root.present(vc, animated: false) navigation.present(viewController: vc)
state = .presented state = .presented
} }
@ -120,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
func finalize(activity: NSUserActivity) { func finalize(activity: NSUserActivity) {
precondition(state > .initial) precondition(state > .initial)
navigation.run()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {

View File

@ -133,13 +133,16 @@ class UserActivityManager {
return activity return activity
} }
func handleCheckNotifications(activity: NSUserActivity) async { func handleCheckNotifications(activity: NSUserActivity) {
await context.select(route: .notifications) context.select(route: .notifications)
context.popToRoot() context.popToRoot()
if let notificationsPageController = context.topViewController as? NotificationsPageViewController { context.withTopViewController { topViewController, completion in
if let notificationsPageController = topViewController as? NotificationsPageViewController {
notificationsPageController.loadViewIfNeeded() notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
} }
completion()
}
} }
static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? { static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? {
@ -204,32 +207,41 @@ class UserActivityManager {
return (timeline, positionInfo) return (timeline, positionInfo)
} }
func handleShowTimeline(activity: NSUserActivity) async { func handleShowTimeline(activity: NSUserActivity) {
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
var timelineVC: TimelineViewController?
if let pinned = PinnedTimeline(timeline: timeline), if let pinned = PinnedTimeline(timeline: timeline),
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
await context.select(route: .timelines) context.select(route: .timelines)
context.popToRoot() context.popToRoot()
let pageController = context.topViewController as! TimelinesPageViewController context.withTopViewController { topViewController, completion in
let pageController = topViewController as! TimelinesPageViewController
pageController.selectTimeline(pinned, animated: false) pageController.selectTimeline(pinned, animated: false)
timelineVC = pageController.currentViewController as? TimelineViewController }
} else if case .list(let id) = timeline { } else if case .list(let id) = timeline {
await context.select(route: .list(id: id)) context.select(route: .list(id: id))
timelineVC = context.topViewController as? TimelineViewController
} else { } else {
await context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
context.push(timelineVC!) context.push(timelineVC)
} }
if let timelineVC, if let positionInfo,
let positionInfo,
context.isHandoff { context.isHandoff {
context.withTopViewController { topViewController, completion in
let timelineVC: TimelineViewController
if let topViewController = topViewController as? TimelineViewController {
timelineVC = topViewController
} else if let topViewController = topViewController as? TimelinesPageViewController {
timelineVC = topViewController.currentViewController as! TimelineViewController
} else {
return
}
Task { Task {
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
completion()
}
} }
} }
} }
@ -249,11 +261,11 @@ class UserActivityManager {
return activity.userInfo?["mainStatusID"] as? String return activity.userInfo?["mainStatusID"] as? String
} }
func handleShowConversation(activity: NSUserActivity) async { func handleShowConversation(activity: NSUserActivity) {
guard let mainStatusID = Self.getConversationStatus(from: activity) else { guard let mainStatusID = Self.getConversationStatus(from: activity) else {
return return
} }
await context.select(route: .timelines) context.select(route: .timelines)
context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController)) context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController))
} }
@ -274,18 +286,19 @@ class UserActivityManager {
return activity.userInfo?["query"] as? String return activity.userInfo?["query"] as? String
} }
func handleSearch(activity: NSUserActivity) async { func handleSearch(activity: NSUserActivity) {
await context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
context.withTopViewController { topViewController, completion in
let searchController: UISearchController let searchController: UISearchController
let resultsController: SearchResultsViewController let resultsController: SearchResultsViewController
if let explore = context.topViewController as? ExploreViewController { if let explore = topViewController as? ExploreViewController {
explore.loadViewIfNeeded() explore.loadViewIfNeeded()
explore.searchControllerStatusOnAppearance = true explore.searchControllerStatusOnAppearance = true
searchController = explore.searchController searchController = explore.searchController
resultsController = explore.resultsController resultsController = explore.resultsController
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController { } else if let inlineTrends = topViewController as? InlineTrendsViewController {
inlineTrends.loadViewIfNeeded() inlineTrends.loadViewIfNeeded()
inlineTrends.searchControllerStatusOnAppearance = true inlineTrends.searchControllerStatusOnAppearance = true
searchController = inlineTrends.searchController searchController = inlineTrends.searchController
@ -302,6 +315,7 @@ class UserActivityManager {
searchController.searchBar.becomeFirstResponder() searchController.searchBar.becomeFirstResponder()
} }
} }
}
static func bookmarksActivity(accountID: String) -> NSUserActivity { static func bookmarksActivity(accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .bookmarks, accountID: accountID) let activity = NSUserActivity(type: .bookmarks, accountID: accountID)
@ -311,8 +325,8 @@ class UserActivityManager {
return activity return activity
} }
func handleBookmarks(activity: NSUserActivity) async { func handleBookmarks(activity: NSUserActivity) {
await context.select(route: .bookmarks) context.select(route: .bookmarks)
} }
// MARK: - My Profile // MARK: - My Profile
@ -325,8 +339,8 @@ class UserActivityManager {
return activity return activity
} }
func handleMyProfile(activity: NSUserActivity) async { func handleMyProfile(activity: NSUserActivity) {
await context.select(route: .myProfile) context.select(route: .myProfile)
} }
// MARK: - Show Profile // MARK: - Show Profile
@ -344,11 +358,11 @@ class UserActivityManager {
return activity.userInfo?["profileID"] as? String return activity.userInfo?["profileID"] as? String
} }
func handleShowProfile(activity: NSUserActivity) async { func handleShowProfile(activity: NSUserActivity) {
guard let accountID = Self.getProfile(from: activity) else { guard let accountID = Self.getProfile(from: activity) else {
return return
} }
await context.select(route: .timelines) context.select(route: .timelines)
context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController)) context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController))
} }
@ -361,11 +375,11 @@ class UserActivityManager {
return activity return activity
} }
func handleShowNotification(activity: NSUserActivity) async { func handleShowNotification(activity: NSUserActivity) {
guard let notificationID = activity.userInfo?["notificationID"] as? String else { guard let notificationID = activity.userInfo?["notificationID"] as? String else {
return return
} }
await context.select(route: .notifications) context.select(route: .notifications)
context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)) context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController))
} }

View File

@ -23,7 +23,7 @@ enum UserActivityType: String {
extension UserActivityType { extension UserActivityType {
@MainActor @MainActor
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void { var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void {
switch self { switch self {
case .mainScene: case .mainScene:
fatalError("cannot handle main scene activity") fatalError("cannot handle main scene activity")