Compare commits

..

2 Commits

Author SHA1 Message Date
Shadowfacts 71fa3910a1 Simplify NSUserActivity construction code 2023-02-22 21:42:09 -05:00
Shadowfacts 75f290ae8f Tab state restoration
Closes #32
2023-02-22 21:38:12 -05:00
17 changed files with 280 additions and 98 deletions

View File

@ -221,6 +221,7 @@
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; }; D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; };
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; }; D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; };
D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */; };
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; }; D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
@ -637,6 +638,7 @@
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; }; D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = "<group>"; }; D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = "<group>"; };
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRestorableViewController.swift; sourceTree = "<group>"; };
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; }; D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; };
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; }; D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
@ -1495,6 +1497,7 @@
D6E0DC8D216EDF1E00369478 /* Previewing.swift */, D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */,
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */, D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
@ -2120,6 +2123,7 @@
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */, D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */, D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */,
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */, D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */,
D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */, D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
D61F75BD293D099600C0B37F /* Lazy.swift in Sources */, D61F75BD293D099600C0B37F /* Lazy.swift in Sources */,

View File

@ -412,6 +412,21 @@ extension ConversationViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController } var apiController: MastodonController! { mastodonController }
} }
extension ConversationViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
if let accountID = mastodonController.accountInfo?.id,
case .localID(let id) = mode {
return UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountID)
} else {
return nil
}
}
func restoreActivity(_ activity: NSUserActivity) {
fatalError("ConversationViewController must be reconstructed, not restored")
}
}
extension ConversationViewController: ToastableViewController { extension ConversationViewController: ToastableViewController {
var toastScrollView: UIScrollView? { var toastScrollView: UIScrollView? {
if case .displaying(let vc) = state { if case .displaying(let vc) = state {

View File

@ -531,6 +531,34 @@ extension ExploreViewController {
} }
} }
extension ExploreViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
if searchController.isActive {
return UserActivityManager.searchActivity(query: searchController.searchBar.text, accountID: mastodonController.accountInfo!.id)
} else {
return nil
}
}
func restoreActivity(_ activity: NSUserActivity) {
guard let type = UserActivityType(rawValue: activity.activityType) else {
return
}
if type == .bookmarks {
show(BookmarksViewController(mastodonController: mastodonController), sender: nil)
} else if type == .search {
loadViewIfNeeded()
searchController.isActive = true
if let query = UserActivityManager.getSearchQuery(from: activity),
!query.isEmpty {
searchController.searchBar.text = query
} else {
searchController.searchBar.becomeFirstResponder()
}
}
}
}
extension ExploreViewController: InstanceTimelineViewControllerDelegate { extension ExploreViewController: InstanceTimelineViewControllerDelegate {
func didSaveInstance(url: URL) { func didSaveInstance(url: URL) {
dismiss(animated: true) { dismiss(animated: true) {
@ -553,7 +581,7 @@ extension ExploreViewController: UICollectionViewDragDelegate {
let provider: NSItemProvider let provider: NSItemProvider
switch item { switch item {
case .bookmarks: case .bookmarks:
let activity = UserActivityManager.bookmarksActivity() let activity = UserActivityManager.bookmarksActivity(accountID: accountID)
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity) provider = NSItemProvider(object: activity)
case let .list(list): case let .list(list):

View File

@ -18,6 +18,8 @@ class BookmarksViewController: LocalPredicateStatusesViewController {
request: { Client.getBookmarks(range: $0) }, request: { Client.getBookmarks(range: $0) },
mastodonController: mastodonController mastodonController: mastodonController
) )
userActivity = UserActivityManager.bookmarksActivity(accountID: mastodonController.accountInfo!.id)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -25,3 +27,12 @@ class BookmarksViewController: LocalPredicateStatusesViewController {
} }
} }
extension BookmarksViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
return UserActivityManager.bookmarksActivity(accountID: mastodonController.accountInfo!.id)
}
func restoreActivity(_ activity: NSUserActivity) {
}
}

View File

@ -304,15 +304,15 @@ class MainSidebarViewController: UIViewController {
switch item { switch item {
case .tab(.notifications): case .tab(.notifications):
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode) return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id)
case .tab(.compose): case .tab(.compose):
return UserActivityManager.newPostActivity(accountID: id) return UserActivityManager.newPostActivity(accountID: id)
case .explore: case .explore:
return UserActivityManager.searchActivity() return UserActivityManager.searchActivity(query: nil, accountID: id)
case .bookmarks: case .bookmarks:
return UserActivityManager.bookmarksActivity() return UserActivityManager.bookmarksActivity(accountID: id)
case .tab(.myProfile): case .tab(.myProfile):
return UserActivityManager.myProfileActivity() return UserActivityManager.myProfileActivity(accountID: id)
case let .list(list): case let .list(list):
return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id) return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id)
case let .savedHashtag(tag): case let .savedHashtag(tag):

View File

@ -398,36 +398,76 @@ extension MainSplitViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController } var apiController: MastodonController! { mastodonController }
} }
extension MainSplitViewController: TuskerRootViewController { extension MainSplitViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? { func stateRestorationActivity() -> NSUserActivity? {
if traitCollection.horizontalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact {
return tabBarViewController.stateRestorationActivity() return tabBarViewController.stateRestorationActivity()
} else { } else if let currentItem = sidebar.selectedItem,
if let timelinePages = navigationStackFor(item: .tab(.timelines))?.first as? TimelinesPageViewController { let navStack = navigationStackFor(item: currentItem),
return timelinePages.stateRestorationActivity() let top = navStack.last as? StateRestorableViewController {
return top.stateRestorationActivity()
} else { } else {
stateRestorationLogger.fault("MainSplitViewController: Unable to create state restoration activity") stateRestorationLogger.fault("MainSplitViewController: Unable to create state restoration activity")
return nil return nil
} }
} }
}
func restoreActivity(_ activity: NSUserActivity) { func restoreActivity(_ activity: NSUserActivity) {
if traitCollection.horizontalSizeClass == .compact { guard traitCollection.horizontalSizeClass != .compact else {
tabBarViewController.restoreActivity(activity) tabBarViewController.restoreActivity(activity)
} else {
if activity.activityType == UserActivityType.showTimeline.rawValue {
guard let timelinePages = navigationStackFor(item: .tab(.timelines))?.first as? TimelinesPageViewController else {
stateRestorationLogger.fault("MainSplitViewController: Unable to restore timeline activity, couldn't find VC")
return return
} }
timelinePages.restoreActivity(activity) guard let type = UserActivityType(rawValue: activity.activityType) else {
} else { return
stateRestorationLogger.fault("MainSplitViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
}
}
} }
let item: MainSidebarViewController.Item
var needsRestore = true
switch type {
case .showTimeline:
item = .tab(.timelines)
case .checkNotifications:
item = .tab(.notifications)
case .search:
item = .explore
case .bookmarks:
item = .bookmarks
case .myProfile:
item = .tab(.myProfile)
needsRestore = false
case .newPost:
return
case .showConversation, .showProfile:
item = .tab(.timelines)
default:
stateRestorationLogger.fault("MainSplitViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
return
}
sidebar.select(item: item, animated: false)
select(item: item)
if type == .showConversation {
if let statusID = UserActivityManager.getConversationStatus(from: activity) {
let conv = ConversationViewController(for: statusID, state: .unknown, mastodonController: mastodonController)
secondaryNavController.show(conv, sender: nil)
}
} else if type == .showProfile {
if let accountID = UserActivityManager.getProfile(from: activity) {
let profile = ProfileViewController(accountID: accountID, mastodonController: mastodonController)
secondaryNavController.show(profile, sender: nil)
}
} else if needsRestore {
if let vc = secondaryNavController.viewControllers.first as? StateRestorableViewController {
vc.restoreActivity(activity)
} else {
stateRestorationLogger.fault("MainSplitViewController: Unable to restore activity, couldn't find StateRestorableViewController")
}
}
}
}
extension MainSplitViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
self.compose() self.compose()
} }

View File

@ -240,14 +240,14 @@ extension MainTabBarViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController } var apiController: MastodonController! { mastodonController }
} }
extension MainTabBarViewController: TuskerRootViewController { extension MainTabBarViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? { func stateRestorationActivity() -> NSUserActivity? {
let nav = viewController(for: .timelines) as! UINavigationController let nav = viewController(for: selectedTab) as! UINavigationController
var activity: NSUserActivity? var activity: NSUserActivity?
if let timelinePages = nav.viewControllers.first as? TimelinesPageViewController { if let vc = nav.topViewController as? StateRestorableViewController {
activity = timelinePages.stateRestorationActivity() activity = vc.stateRestorationActivity()
} else { } else {
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find timeline/page VC") stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
} }
if let presentedNav = presentedViewController as? UINavigationController, if let presentedNav = presentedViewController as? UINavigationController,
let compose = presentedNav.viewControllers.first as? ComposeHostingController { let compose = presentedNav.viewControllers.first as? ComposeHostingController {
@ -257,6 +257,10 @@ extension MainTabBarViewController: TuskerRootViewController {
} }
func restoreActivity(_ activity: NSUserActivity) { func restoreActivity(_ activity: NSUserActivity) {
guard let type = UserActivityType(rawValue: activity.activityType) else {
return
}
func restoreEditedDraft() { func restoreEditedDraft() {
// on iOS 16+, this is handled by the duckable container // on iOS 16+, this is handled by the duckable container
if #unavailable(iOS 16.0), if #unavailable(iOS 16.0),
@ -265,22 +269,50 @@ extension MainTabBarViewController: TuskerRootViewController {
} }
} }
if activity.activityType == UserActivityType.showTimeline.rawValue { let tab: Tab
let nav = viewController(for: .timelines) as! UINavigationController switch type {
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController else { case .showTimeline:
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore timeline activity, couldn't find VC") tab = .timelines
return case .checkNotifications:
} tab = .notifications
timelinePages.restoreActivity(activity) case .search, .bookmarks:
restoreEditedDraft() tab = .explore
} else if activity.activityType == UserActivityType.newPost.rawValue { case .myProfile:
tab = .myProfile
case .newPost:
restoreEditedDraft() restoreEditedDraft()
return return
} else { case .showConversation, .showProfile:
tab = .timelines
default:
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)") stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
} return
} }
select(tab: tab)
let nav = viewController(for: tab) as! UINavigationController
if type == .showConversation {
if let statusID = UserActivityManager.getConversationStatus(from: activity) {
let conv = ConversationViewController(for: statusID, state: .unknown, mastodonController: mastodonController)
nav.pushViewController(conv, animated: false)
}
} else if type == .showProfile {
if let accountID = UserActivityManager.getProfile(from: activity) {
let profile = ProfileViewController(accountID: accountID, mastodonController: mastodonController)
nav.pushViewController(profile, animated: false)
}
} else if type == .bookmarks {
nav.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: false)
} else if let vc = nav.viewControllers.first as? StateRestorableViewController {
vc.restoreActivity(activity)
} else {
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity, couldn't find StateRestorableViewController")
}
}
}
extension MainTabBarViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
compose() compose()
} }

View File

@ -8,9 +8,7 @@
import UIKit import UIKit
protocol TuskerRootViewController: UIViewController, StatusBarTappableViewController { protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
func stateRestorationActivity() -> NSUserActivity?
func restoreActivity(_ activity: NSUserActivity)
func presentCompose() func presentCompose()
func select(tab: MainTabBarViewController.Tab) func select(tab: MainTabBarViewController.Tab)
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?

View File

@ -22,7 +22,7 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
super.init(pages: [.all, .mentions]) { page in super.init(pages: [.all, .mentions]) { page in
let vc = NotificationsTableViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController) let vc = NotificationsTableViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
vc.title = page.title vc.title = page.title
vc.userActivity = page.userActivity vc.userActivity = page.userActivity(accountID: mastodonController.accountInfo!.id)
return vc return vc
} }
@ -48,7 +48,6 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
case .mentionsOnly: case .mentionsOnly:
page = .mentions page = .mentions
} }
segmentedControl.setSelectedOption(page, animated: false)
selectPage(page, animated: false) selectPage(page, animated: false)
} }
@ -80,14 +79,26 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
} }
} }
var userActivity: NSUserActivity { func userActivity(accountID: String) -> NSUserActivity {
switch self { switch self {
case .all: case .all:
return UserActivityManager.checkNotificationsActivity(mode: .allNotifications) return UserActivityManager.checkNotificationsActivity(mode: .allNotifications, accountID: accountID)
case .mentions: case .mentions:
return UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly) return UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly, accountID: accountID)
} }
} }
} }
} }
extension NotificationsPageViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
}
func restoreActivity(_ activity: NSUserActivity) {
if let mode = UserActivityManager.getNotificationsMode(from: activity) {
selectMode(mode)
}
}
}

View File

@ -39,7 +39,11 @@ class MyProfileViewController: ProfileViewController {
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
userActivity = UserActivityManager.myProfileActivity() userActivity = UserActivityManager.myProfileActivity(accountID: mastodonController.accountInfo!.id)
}
override func stateRestorationActivity() -> NSUserActivity? {
return UserActivityManager.myProfileActivity(accountID: mastodonController.accountInfo!.id)
} }
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) { private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {

View File

@ -10,7 +10,7 @@ import UIKit
import Pachyderm import Pachyderm
import Combine import Combine
class ProfileViewController: UIViewController { class ProfileViewController: UIViewController, StateRestorableViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
@ -285,6 +285,20 @@ class ProfileViewController: UIViewController {
compose(editing: draft) compose(editing: draft)
} }
} }
// MARK: StateRestorableViewController
func stateRestorationActivity() -> NSUserActivity? {
if let accountID,
let accountInfo = mastodonController.accountInfo {
return UserActivityManager.showProfileActivity(id: accountID, accountID: accountInfo.id)
} else {
return nil
}
}
func restoreActivity(_ activity: NSUserActivity) {
fatalError("ProfileViewController must be reconstructed, not restored")
}
} }
extension ProfileViewController { extension ProfileViewController {

View File

@ -104,7 +104,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
.filter { $0 != self.currentQuery } .filter { $0 != self.currentQuery }
.sink(receiveValue: performSearch(query:)) .sink(receiveValue: performSearch(query:))
userActivity = UserActivityManager.searchActivity() userActivity = UserActivityManager.searchActivity(query: nil, accountID: mastodonController.accountInfo!.id)
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
} }

View File

@ -105,22 +105,6 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated) self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
} }
func stateRestorationActivity() -> NSUserActivity? {
return (currentViewController as? TimelineViewController)?.stateRestorationActivity()
}
func restoreActivity(_ activity: NSUserActivity) {
guard let timeline = UserActivityManager.getTimeline(from: activity),
let pinned = PinnedTimeline(timeline: timeline) else {
return
}
let page = Page(mastodonController: mastodonController, timeline: pinned)
// the pinned timelines may have changed after an iCloud sync, in which case don't restore anything
if pages.contains(page) {
selectPage(page, animated: false)
}
}
@objc private func customizePressed() { @objc private func customizePressed() {
present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true) present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true)
} }
@ -222,3 +206,21 @@ extension TimelinesPageViewController: TimelineViewControllerDelegate {
} }
} }
} }
extension TimelinesPageViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
return (currentViewController as? TimelineViewController)?.stateRestorationActivity()
}
func restoreActivity(_ activity: NSUserActivity) {
guard let timeline = UserActivityManager.getTimeline(from: activity),
let pinned = PinnedTimeline(timeline: timeline) else {
return
}
let page = Page(mastodonController: mastodonController, timeline: pinned)
// the pinned timelines may have changed after an iCloud sync, in which case don't restore anything
if pages.contains(page) {
selectPage(page, animated: false)
}
}
}

View File

@ -19,7 +19,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
private var pageControllers = [Page: UIViewController]() private var pageControllers = [Page: UIViewController]()
private var initialPage: Page private var initialPage: Page
private var currentPage: Page private(set) var currentPage: Page
var currentIndex: Int! { var currentIndex: Int! {
pages.firstIndex(of: currentPage) pages.firstIndex(of: currentPage)
} }

View File

@ -0,0 +1,15 @@
//
// StateRestorableViewController.swift
// Tusker
//
// Created by Shadowfacts on 2/22/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
protocol StateRestorableViewController: UIViewController {
func stateRestorationActivity() -> NSUserActivity?
func restoreActivity(_ activity: NSUserActivity)
}

View File

@ -34,8 +34,11 @@ extension NSUserActivity {
} }
} }
convenience init(type: UserActivityType) { convenience init(type: UserActivityType, accountID: String) {
self.init(activityType: type.rawValue) self.init(activityType: type.rawValue)
self.userInfo = [
"accountID": accountID
]
} }
func handleResume(manager: UserActivityManager) -> Bool { func handleResume(manager: UserActivityManager) -> Bool {

View File

@ -47,7 +47,7 @@ class UserActivityManager {
// MARK: - Main Scene // MARK: - Main Scene
static func mainSceneActivity(accountID: String?) -> NSUserActivity { static func mainSceneActivity(accountID: String?) -> NSUserActivity {
let activity = NSUserActivity(type: .mainScene) let activity = NSUserActivity(activityType: UserActivityType.mainScene.rawValue)
if let accountID { if let accountID {
activity.userInfo = [ activity.userInfo = [
"accountID": accountID, "accountID": accountID,
@ -59,11 +59,8 @@ class UserActivityManager {
// MARK: - New Post // MARK: - New Post
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity { static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
// todo: update to use managed objects // todo: update to use managed objects
let activity = NSUserActivity(type: .newPost) let activity = NSUserActivity(type: .newPost, accountID: accountID)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.userInfo = [
"accountID": accountID,
]
if let mentioning = mentioning { if let mentioning = mentioning {
activity.userInfo!["mentioning"] = mentioning.acct activity.userInfo!["mentioning"] = mentioning.acct
activity.title = "Send a message to \(mentioning.displayName)" activity.title = "Send a message to \(mentioning.displayName)"
@ -85,11 +82,10 @@ class UserActivityManager {
} }
static func editDraftActivity(id: UUID, accountID: String) -> NSUserActivity { static func editDraftActivity(id: UUID, accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .newPost) let activity = NSUserActivity(type: .newPost, accountID: accountID)
activity.userInfo = [ activity.addUserInfoEntries(from: [
"accountID": accountID,
"draftID": id.uuidString, "draftID": id.uuidString,
] ])
return activity return activity
} }
@ -130,8 +126,8 @@ class UserActivityManager {
} }
// MARK: - Check Notifications // MARK: - Check Notifications
static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications) -> NSUserActivity { static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications, accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .checkNotifications) let activity = NSUserActivity(type: .checkNotifications, accountID: accountID)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.addUserInfoEntries(from: [ activity.addUserInfoEntries(from: [
"notificationsMode": mode.rawValue "notificationsMode": mode.rawValue
@ -169,12 +165,11 @@ class UserActivityManager {
static func showTimelineActivity(timeline: Timeline, accountID: String) -> NSUserActivity? { static func showTimelineActivity(timeline: Timeline, accountID: String) -> NSUserActivity? {
guard let timelineData = try? encoder.encode(timeline) else { return nil } guard let timelineData = try? encoder.encode(timeline) else { return nil }
let activity = NSUserActivity(type: .showTimeline) let activity = NSUserActivity(type: .showTimeline, accountID: accountID)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.userInfo = [ activity.addUserInfoEntries(from: [
"timelineData": timelineData, "timelineData": timelineData,
"accountID": accountID, ])
]
switch timeline { switch timeline {
case .home: case .home:
activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title") activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title")
@ -229,11 +224,10 @@ class UserActivityManager {
// MARK: - Show Conversation // MARK: - Show Conversation
static func showConversationActivity(mainStatusID: String, accountID: String, isEligibleForPrediction: Bool = false) -> NSUserActivity { static func showConversationActivity(mainStatusID: String, accountID: String, isEligibleForPrediction: Bool = false) -> NSUserActivity {
let activity = NSUserActivity(type: .showConversation) let activity = NSUserActivity(type: .showConversation, accountID: accountID)
activity.userInfo = [ activity.addUserInfoEntries(from: [
"mainStatusID": mainStatusID, "mainStatusID": mainStatusID,
"accountID": accountID, ])
]
activity.isEligibleForPrediction = isEligibleForPrediction activity.isEligibleForPrediction = isEligibleForPrediction
return activity return activity
} }
@ -244,14 +238,21 @@ class UserActivityManager {
// MARK: - Explore // MARK: - Explore
static func searchActivity() -> NSUserActivity { static func searchActivity(query: String?, accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .search) let activity = NSUserActivity(type: .search, accountID: accountID)
if let query {
activity.userInfo!["query"] = query
}
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.title = NSLocalizedString("Search", comment: "search shortcut title") activity.title = NSLocalizedString("Search", comment: "search shortcut title")
activity.suggestedInvocationPhrase = NSLocalizedString("Search the fediverse", comment: "search shortcut invocation phrase") activity.suggestedInvocationPhrase = NSLocalizedString("Search the fediverse", comment: "search shortcut invocation phrase")
return activity return activity
} }
static func getSearchQuery(from activity: NSUserActivity) -> String? {
return activity.userInfo?["query"] as? String
}
func handleSearch(activity: NSUserActivity) { func handleSearch(activity: NSUserActivity) {
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .explore) mainViewController.select(tab: .explore)
@ -260,12 +261,17 @@ class UserActivityManager {
navigationController.popToRootViewController(animated: false) navigationController.popToRootViewController(animated: false)
exploreController.loadViewIfNeeded() exploreController.loadViewIfNeeded()
exploreController.searchController.isActive = true exploreController.searchController.isActive = true
if let query = Self.getSearchQuery(from: activity),
!query.isEmpty {
exploreController.searchController.searchBar.text = query
} else {
exploreController.searchController.searchBar.becomeFirstResponder() exploreController.searchController.searchBar.becomeFirstResponder()
} }
} }
}
static func bookmarksActivity() -> NSUserActivity { static func bookmarksActivity(accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .bookmarks) let activity = NSUserActivity(type: .bookmarks, accountID: accountID)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.title = NSLocalizedString("View Bookmarks", comment: "bookmarks shortcut title") activity.title = NSLocalizedString("View Bookmarks", comment: "bookmarks shortcut title")
activity.suggestedInvocationPhrase = NSLocalizedString("Show my bookmarks in Tusker", comment: "bookmarks shortcut invocation phrase") activity.suggestedInvocationPhrase = NSLocalizedString("Show my bookmarks in Tusker", comment: "bookmarks shortcut invocation phrase")
@ -282,8 +288,8 @@ class UserActivityManager {
} }
// MARK: - My Profile // MARK: - My Profile
static func myProfileActivity() -> NSUserActivity { static func myProfileActivity(accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .myProfile) let activity = NSUserActivity(type: .myProfile, accountID: accountID)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.title = NSLocalizedString("My Profile", comment: "my profile shortcut title") activity.title = NSLocalizedString("My Profile", comment: "my profile shortcut title")
activity.suggestedInvocationPhrase = NSLocalizedString("Show my Mastodon profile", comment: "my profile shortuct invocation phrase") activity.suggestedInvocationPhrase = NSLocalizedString("Show my Mastodon profile", comment: "my profile shortuct invocation phrase")
@ -297,12 +303,11 @@ class UserActivityManager {
// MARK: - Show Profile // MARK: - Show Profile
static func showProfileActivity(id profileID: String, accountID: String) -> NSUserActivity { static func showProfileActivity(id profileID: String, accountID: String) -> NSUserActivity {
let activity = NSUserActivity(type: .showProfile) let activity = NSUserActivity(type: .showProfile, accountID: accountID)
activity.userInfo = [ activity.addUserInfoEntries(from: [
"profileID": profileID, "profileID": profileID,
"accountID": accountID, ])
] activity.isEligibleForPrediction = true
// todo: should this be eligible for prediction?
return activity return activity
} }