parent
d74be9d81d
commit
a3e64703ab
|
@ -74,7 +74,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
|
||||||
private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? {
|
private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? {
|
||||||
switch UserActivityType(rawValue: activity.activityType) {
|
switch UserActivityType(rawValue: activity.activityType) {
|
||||||
case .showTimeline:
|
case .showTimeline:
|
||||||
guard let timeline = UserActivityManager.getTimeline(from: activity) else { return nil }
|
guard let (timeline, _) = UserActivityManager.getTimeline(from: activity) else { return nil }
|
||||||
return timelineViewController(for: timeline, mastodonController: mastodonController)
|
return timelineViewController(for: timeline, mastodonController: mastodonController)
|
||||||
|
|
||||||
case .showConversation:
|
case .showConversation:
|
||||||
|
|
|
@ -70,7 +70,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
stateRestorationLogger.info("MainSceneDelegate cannot resume user activity for different account")
|
stateRestorationLogger.info("MainSceneDelegate cannot resume user activity for different account")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
context = ActiveAccountUserActivityHandlingContext(root: rootViewController!)
|
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||||
}
|
}
|
||||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
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 {
|
||||||
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(root: rootViewController!))
|
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(isHandoff: false, root: rootViewController!))
|
||||||
} else {
|
} else {
|
||||||
stateRestorationLogger.fault("MainSceneDelegate launched with non-restorable activity \(activity.activityType, privacy: .public)")
|
stateRestorationLogger.fault("MainSceneDelegate launched with non-restorable activity \(activity.activityType, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var userActivityNeedsUpdate = PassthroughSubject<Void, Never>()
|
||||||
private var contentOffsetObservation: NSKeyValueObservation?
|
private var contentOffsetObservation: NSKeyValueObservation?
|
||||||
// the last time this VC disappeared or the scene was backgrounded while it was active, used to decide if we want to check for present when reappearing
|
// the last time this VC disappeared or the scene was backgrounded while it was active, used to decide if we want to check for present when reappearing
|
||||||
private var disappearedAt: Date?
|
private var disappearedAt: Date?
|
||||||
|
@ -171,6 +172,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
|
|
||||||
|
if userActivity != nil {
|
||||||
|
userActivityNeedsUpdate
|
||||||
|
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [unowned self] _ in
|
||||||
|
if let info = self.timelinePositionInfo(),
|
||||||
|
let userActivity = self.userActivity {
|
||||||
|
UserActivityManager.addTimelinePositionInfo(to: userActivity, statusIDs: info.statusIDs, centerStatusID: info.centerStatusID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// separate method because InstanceTimelineViewController needs to be able to customize it
|
// separate method because InstanceTimelineViewController needs to be able to customize it
|
||||||
|
@ -294,15 +307,13 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
return centerVisible
|
return centerVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveState() {
|
private func timelinePositionInfo() -> (statusIDs: [String], centerStatusID: String)? {
|
||||||
guard isViewLoaded,
|
guard isViewLoaded else {
|
||||||
persistsState,
|
return nil
|
||||||
let accountInfo = mastodonController.accountInfo else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
guard let centerVisible = currentCenterVisibleIndexPath(snapshot: snapshot) else {
|
guard let centerVisible = currentCenterVisibleIndexPath(snapshot: snapshot) else {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
||||||
|
|
||||||
|
@ -342,6 +353,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
} else {
|
} else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
return (ids, centerVisibleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveState() {
|
||||||
|
guard persistsState,
|
||||||
|
let accountInfo = mastodonController.accountInfo,
|
||||||
|
let (ids, centerVisibleID) = timelinePositionInfo() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
switch Preferences.shared.timelineSyncMode {
|
switch Preferences.shared.timelineSyncMode {
|
||||||
case .icloud:
|
case .icloud:
|
||||||
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
|
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
|
||||||
|
@ -409,6 +429,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
return loaded
|
return loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restoreStateFromHandoff(statusIDs: [String], centerStatusID: String) async {
|
||||||
|
let crumb = Breadcrumb(level: .debug, category: "TimelineViewController")
|
||||||
|
crumb.message = "Restoring state from handoff activity"
|
||||||
|
SentrySDK.addBreadcrumb(crumb)
|
||||||
|
await controller.restoreInitial { @MainActor in
|
||||||
|
let position = TimelinePosition(context: mastodonController.persistentContainer.viewContext)
|
||||||
|
position.statusIDs = statusIDs
|
||||||
|
position.centerStatusID = centerStatusID
|
||||||
|
let hasStatusesToRestore = await loadStatusesToRestore(position: position)
|
||||||
|
if hasStatusesToRestore {
|
||||||
|
applyItemsToRestore(position: position)
|
||||||
|
}
|
||||||
|
mastodonController.persistentContainer.viewContext.delete(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
|
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
|
||||||
let originalPositionStatusIDs = position.statusIDs
|
let originalPositionStatusIDs = position.statusIDs
|
||||||
|
@ -1293,6 +1329,16 @@ extension TimelineViewController: UICollectionViewDelegate {
|
||||||
removeTimelineDescriptionCell()
|
removeTimelineDescriptionCell()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if !decelerate {
|
||||||
|
userActivityNeedsUpdate.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
userActivityNeedsUpdate.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: UICollectionViewDragDelegate {
|
extension TimelineViewController: UICollectionViewDragDelegate {
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Duckable
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol UserActivityHandlingContext {
|
protocol UserActivityHandlingContext {
|
||||||
|
var isHandoff: Bool { get }
|
||||||
|
|
||||||
func select(route: TuskerRoute)
|
func select(route: TuskerRoute)
|
||||||
func present(_ vc: UIViewController)
|
func present(_ vc: UIViewController)
|
||||||
|
|
||||||
|
@ -24,6 +26,7 @@ protocol UserActivityHandlingContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
||||||
|
let isHandoff: Bool
|
||||||
let root: TuskerRootViewController
|
let root: TuskerRootViewController
|
||||||
var navigationDelegate: TuskerNavigationDelegate {
|
var navigationDelegate: TuskerNavigationDelegate {
|
||||||
root.getNavigationDelegate()!
|
root.getNavigationDelegate()!
|
||||||
|
@ -63,6 +66,8 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
||||||
self.root = root
|
self.root = root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isHandoff: Bool { false }
|
||||||
|
|
||||||
func select(route: TuskerRoute) {
|
func select(route: TuskerRoute) {
|
||||||
root.select(route: route, animated: false)
|
root.select(route: route, animated: false)
|
||||||
state = .selectedRoute
|
state = .selectedRoute
|
||||||
|
|
|
@ -189,30 +189,53 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getTimeline(from activity: NSUserActivity) -> Timeline? {
|
static func addTimelinePositionInfo(to activity: NSUserActivity, statusIDs: [String], centerStatusID: String) {
|
||||||
|
activity.addUserInfoEntries(from: [
|
||||||
|
"statusIDs": statusIDs,
|
||||||
|
"centerStatusID": centerStatusID
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getTimeline(from activity: NSUserActivity) -> (Timeline, (statusIDs: [String], centerStatusID: String)?)? {
|
||||||
guard activity.activityType == UserActivityType.showTimeline.rawValue,
|
guard activity.activityType == UserActivityType.showTimeline.rawValue,
|
||||||
let data = activity.userInfo?["timelineData"] as? Data else {
|
let data = activity.userInfo?["timelineData"] as? Data,
|
||||||
|
let timeline = try? UserActivityManager.decoder.decode(Timeline.self, from: data) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return try? UserActivityManager.decoder.decode(Timeline.self, from: data)
|
var positionInfo: ([String], String)?
|
||||||
|
if let ids = activity.userInfo?["statusIDs"] as? [String],
|
||||||
|
let center = activity.userInfo?["centerStatusID"] as? String {
|
||||||
|
positionInfo = (ids, center)
|
||||||
|
}
|
||||||
|
return (timeline, positionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleShowTimeline(activity: NSUserActivity) {
|
func handleShowTimeline(activity: NSUserActivity) {
|
||||||
guard let timeline = Self.getTimeline(from: activity) else { return }
|
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
|
||||||
|
|
||||||
|
let timelineVC: TimelineViewController
|
||||||
if let pinned = PinnedTimeline(timeline: timeline),
|
if let pinned = PinnedTimeline(timeline: timeline),
|
||||||
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
||||||
context.select(route: .timelines)
|
context.select(route: .timelines)
|
||||||
context.popToRoot()
|
context.popToRoot()
|
||||||
let rootController = context.topViewController as! TimelinesPageViewController
|
let pageController = context.topViewController as! TimelinesPageViewController
|
||||||
rootController.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 {
|
||||||
context.select(route: .list(id: id))
|
context.select(route: .list(id: id))
|
||||||
|
timelineVC = context.topViewController! as! TimelineViewController
|
||||||
} else {
|
} else {
|
||||||
context.select(route: .explore)
|
context.select(route: .explore)
|
||||||
context.popToRoot()
|
context.popToRoot()
|
||||||
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||||
context.push(timeline)
|
context.push(timelineVC)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let positionInfo,
|
||||||
|
context.isHandoff {
|
||||||
|
Task {
|
||||||
|
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue