parent
d74be9d81d
commit
a3e64703ab
|
@ -74,7 +74,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
|
|||
private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? {
|
||||
switch UserActivityType(rawValue: activity.activityType) {
|
||||
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)
|
||||
|
||||
case .showConversation:
|
||||
|
|
|
@ -70,7 +70,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
stateRestorationLogger.info("MainSceneDelegate cannot resume user activity for different account")
|
||||
return
|
||||
} else {
|
||||
context = ActiveAccountUserActivityHandlingContext(root: rootViewController!)
|
||||
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||
}
|
||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
if activity.isStateRestorationActivity {
|
||||
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
||||
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
||||
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(root: rootViewController!))
|
||||
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(isHandoff: false, root: rootViewController!))
|
||||
} else {
|
||||
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 var cancellables = Set<AnyCancellable>()
|
||||
private var userActivityNeedsUpdate = PassthroughSubject<Void, Never>()
|
||||
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
|
||||
private var disappearedAt: Date?
|
||||
|
@ -171,6 +172,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
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
|
||||
|
@ -294,15 +307,13 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
return centerVisible
|
||||
}
|
||||
|
||||
private func saveState() {
|
||||
guard isViewLoaded,
|
||||
persistsState,
|
||||
let accountInfo = mastodonController.accountInfo else {
|
||||
return
|
||||
private func timelinePositionInfo() -> (statusIDs: [String], centerStatusID: String)? {
|
||||
guard isViewLoaded else {
|
||||
return nil
|
||||
}
|
||||
let snapshot = dataSource.snapshot()
|
||||
guard let centerVisible = currentCenterVisibleIndexPath(snapshot: snapshot) else {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
||||
|
||||
|
@ -342,6 +353,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
} else {
|
||||
fatalError()
|
||||
}
|
||||
return (ids, centerVisibleID)
|
||||
}
|
||||
|
||||
private func saveState() {
|
||||
guard persistsState,
|
||||
let accountInfo = mastodonController.accountInfo,
|
||||
let (ids, centerVisibleID) = timelinePositionInfo() else {
|
||||
return
|
||||
}
|
||||
switch Preferences.shared.timelineSyncMode {
|
||||
case .icloud:
|
||||
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
|
||||
|
@ -409,6 +429,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
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
|
||||
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
|
||||
let originalPositionStatusIDs = position.statusIDs
|
||||
|
@ -1293,6 +1329,16 @@ extension TimelineViewController: UICollectionViewDelegate {
|
|||
removeTimelineDescriptionCell()
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
userActivityNeedsUpdate.send()
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
userActivityNeedsUpdate.send()
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: UICollectionViewDragDelegate {
|
||||
|
|
|
@ -11,6 +11,8 @@ import Duckable
|
|||
|
||||
@MainActor
|
||||
protocol UserActivityHandlingContext {
|
||||
var isHandoff: Bool { get }
|
||||
|
||||
func select(route: TuskerRoute)
|
||||
func present(_ vc: UIViewController)
|
||||
|
||||
|
@ -24,6 +26,7 @@ protocol UserActivityHandlingContext {
|
|||
}
|
||||
|
||||
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
||||
let isHandoff: Bool
|
||||
let root: TuskerRootViewController
|
||||
var navigationDelegate: TuskerNavigationDelegate {
|
||||
root.getNavigationDelegate()!
|
||||
|
@ -63,6 +66,8 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
self.root = root
|
||||
}
|
||||
|
||||
var isHandoff: Bool { false }
|
||||
|
||||
func select(route: TuskerRoute) {
|
||||
root.select(route: route, animated: false)
|
||||
state = .selectedRoute
|
||||
|
|
|
@ -189,30 +189,53 @@ class UserActivityManager {
|
|||
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,
|
||||
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 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) {
|
||||
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),
|
||||
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
||||
context.select(route: .timelines)
|
||||
context.popToRoot()
|
||||
let rootController = context.topViewController as! TimelinesPageViewController
|
||||
rootController.selectTimeline(pinned, animated: false)
|
||||
let pageController = context.topViewController as! TimelinesPageViewController
|
||||
pageController.selectTimeline(pinned, animated: false)
|
||||
timelineVC = pageController.currentViewController as! TimelineViewController
|
||||
} else if case .list(let id) = timeline {
|
||||
context.select(route: .list(id: id))
|
||||
timelineVC = context.topViewController! as! TimelineViewController
|
||||
} else {
|
||||
context.select(route: .explore)
|
||||
context.popToRoot()
|
||||
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
context.push(timeline)
|
||||
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
context.push(timelineVC)
|
||||
}
|
||||
|
||||
if let positionInfo,
|
||||
context.isHandoff {
|
||||
Task {
|
||||
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue