forked from shadowfacts/Tusker
Persist timeline state using CoreData, rather than NSUserActivity
This allows persisting state for all the primary timelines, and across all accounts. Closes #297 Closes #293
This commit is contained in:
parent
3691c3f483
commit
ba2c34fdd6
|
@ -305,6 +305,7 @@
|
||||||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
||||||
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
|
@ -698,6 +699,7 @@
|
||||||
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
|
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
|
||||||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
||||||
|
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
|
@ -933,6 +935,7 @@
|
||||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
||||||
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
||||||
|
D6D706A62948D4D0000827ED /* TimlineState.swift */,
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2071,6 +2074,7 @@
|
||||||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
||||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||||
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
|
|
|
@ -355,6 +355,15 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTimelineState(timeline: Timeline) -> TimelineState? {
|
||||||
|
do {
|
||||||
|
let req = TimelineState.fetchRequest(timeline: timeline)
|
||||||
|
return try viewContext.fetch(req).first
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
||||||
if changes.hashtags {
|
if changes.hashtags {
|
||||||
|
|
|
@ -18,6 +18,12 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
return NSFetchRequest<StatusMO>(entityName: "Status")
|
return NSFetchRequest<StatusMO>(entityName: "Status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(id: String) -> NSFetchRequest<StatusMO> {
|
||||||
|
let req = Self.fetchRequest()
|
||||||
|
req.predicate = NSPredicate(format: "id = %@", id)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
@NSManaged public var applicationName: String?
|
@NSManaged public var applicationName: String?
|
||||||
@NSManaged private var attachmentsData: Data?
|
@NSManaged private var attachmentsData: Data?
|
||||||
@NSManaged private var bookmarkedInternal: Bool
|
@NSManaged private var bookmarkedInternal: Bool
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// TimlineState.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/13/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@objc(TimelineState)
|
||||||
|
public final class TimelineState: NSManagedObject {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(timeline: Timeline) -> NSFetchRequest<TimelineState> {
|
||||||
|
let req = NSFetchRequest<TimelineState>(entityName: "TimelineState")
|
||||||
|
req.predicate = NSPredicate(format: "timelineKind = %@", toTimelineKind(timeline))
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged private var timelineKind: String
|
||||||
|
@NSManaged public var centerStatusID: String?
|
||||||
|
@NSManaged private var statuses: NSOrderedSet
|
||||||
|
|
||||||
|
var timeline: Timeline {
|
||||||
|
get { fromTimelineKind(timelineKind) }
|
||||||
|
set { timelineKind = toTimelineKind(newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusMOs: [StatusMO] {
|
||||||
|
statuses.array as! [StatusMO]
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(timeline: Timeline, context: NSManagedObjectContext) {
|
||||||
|
self.init(context: context)
|
||||||
|
self.timeline = timeline
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStatuses(_ statusIDs: [String]) {
|
||||||
|
let context = managedObjectContext!
|
||||||
|
// todo: this feels really inefficient, but I'm not sure if it's better or worse than doing a single "id IN %@" fetch and sorting after
|
||||||
|
let mos = statusIDs.compactMap { try? context.fetch(StatusMO.fetchRequest(id: $0)).first }
|
||||||
|
self.statuses = NSOrderedSet(array: mos)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate
|
||||||
|
private func toTimelineKind(_ timeline: Timeline) -> String {
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
return "home"
|
||||||
|
case .public(local: true):
|
||||||
|
return "local"
|
||||||
|
case .public(local: false):
|
||||||
|
return "federated"
|
||||||
|
case .direct:
|
||||||
|
return "direct"
|
||||||
|
case .tag(hashtag: let name):
|
||||||
|
return "hashtag:\(name)"
|
||||||
|
case .list(id: let id):
|
||||||
|
return "list:\(id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fromTimelineKind(_ kind: String) -> Timeline {
|
||||||
|
if kind == "home" {
|
||||||
|
return .home
|
||||||
|
} else if kind == "local" {
|
||||||
|
return .public(local: true)
|
||||||
|
} else if kind == "federated" {
|
||||||
|
return .public(local: false)
|
||||||
|
} else if kind == "direct" {
|
||||||
|
return .direct
|
||||||
|
} else if kind.starts(with: "hashtag:") {
|
||||||
|
return .tag(hashtag: String(trimmingPrefix("hashtag:", of: kind)))
|
||||||
|
} else if kind.starts(with: "list:") {
|
||||||
|
return .list(id: String(trimmingPrefix("list:", of: kind)))
|
||||||
|
} else {
|
||||||
|
fatalError("invalid timeline kind \(kind)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace with Collection.trimmingPrefix
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
private func trimmingPrefix(_ prefix: String, of str: String) -> Substring {
|
||||||
|
return str[str.index(str.startIndex, offsetBy: prefix.count)...]
|
||||||
|
}
|
|
@ -105,10 +105,16 @@
|
||||||
<attribute name="visibilityString" attributeType="String"/>
|
<attribute name="visibilityString" attributeType="String"/>
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
||||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||||
|
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineState" inverseName="statuses" inverseEntity="TimelineState"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="id"/>
|
<constraint value="id"/>
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="TimelineState" representedClassName="TimelineState" syncable="YES">
|
||||||
|
<attribute name="centerStatusID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="timelineKind" attributeType="String" valueTransformerName="pachydermTimeline" customClassName="Tusker.TimelineContainer"/>
|
||||||
|
<relationship name="statuses" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Status" inverseName="timelines" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
</model>
|
</model>
|
|
@ -15,6 +15,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
|
||||||
|
var persistsState = false
|
||||||
|
|
||||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||||
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
// stored separately because i don't want to query the snapshot every time the user scrolls
|
// stored separately because i don't want to query the snapshot every time the user scrolls
|
||||||
|
@ -196,7 +198,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .notLoadedInitial = controller.state {
|
if case .notLoadedInitial = controller.state {
|
||||||
if doRestore() {
|
if restoreState() {
|
||||||
Task {
|
Task {
|
||||||
await checkPresent(jumpImmediately: false)
|
await checkPresent(jumpImmediately: false)
|
||||||
}
|
}
|
||||||
|
@ -227,22 +229,23 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
disappearedAt = Date()
|
disappearedAt = Date()
|
||||||
|
saveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
private func saveState() {
|
||||||
guard isViewLoaded else {
|
guard isViewLoaded,
|
||||||
return nil
|
persistsState else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let visible = collectionView.indexPathsForVisibleItems.sorted()
|
let visible = collectionView.indexPathsForVisibleItems.sorted()
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
|
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
|
||||||
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
|
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
|
||||||
guard let currentAccountID = mastodonController.accountInfo?.id,
|
guard !visible.isEmpty,
|
||||||
!visible.isEmpty,
|
|
||||||
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
|
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
|
||||||
let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
|
let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
|
||||||
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
|
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
||||||
|
|
||||||
|
@ -282,44 +285,40 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
} else {
|
} else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(centerVisibleID)")
|
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
|
||||||
|
|
||||||
|
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) ?? TimelineState(timeline: timeline, context: mastodonController.persistentContainer.viewContext)
|
||||||
|
state.setStatuses(ids)
|
||||||
|
state.centerStatusID = centerVisibleID
|
||||||
|
mastodonController.persistentContainer.save(context: mastodonController.persistentContainer.viewContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
|
guard isViewLoaded,
|
||||||
|
let currentAccountID = mastodonController.accountInfo?.id else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
|
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
|
||||||
activity.addUserInfoEntries(from: [
|
|
||||||
"statusIDs": ids,
|
|
||||||
"centerID": centerVisibleID,
|
|
||||||
])
|
|
||||||
activity.isEligibleForPrediction = false
|
activity.isEligibleForPrediction = false
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
private func restoreState() -> Bool {
|
||||||
self.activityToRestore = activity
|
guard persistsState,
|
||||||
}
|
Preferences.shared.timelineStateRestoration,
|
||||||
|
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) else {
|
||||||
private func doRestore() -> Bool {
|
|
||||||
guard let activity = activityToRestore,
|
|
||||||
Preferences.shared.timelineStateRestoration else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let statusIDs = activity.userInfo?["statusIDs"] as? [String] else {
|
|
||||||
stateRestorationLogger.fault("TimelineViewController: activity missing statusIDs")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
activityToRestore = nil
|
|
||||||
let existingStatuses = statusIDs.filter { mastodonController.persistentContainer.status(for: $0) != nil }
|
|
||||||
guard !existingStatuses.isEmpty else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
let statusIDs = state.statusMOs.map(\.id)
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
controller.restoreInitial {
|
controller.restoreInitial {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
let items = existingStatuses.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) }
|
let items = statusIDs.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) }
|
||||||
snapshot.appendItems(items, toSection: .statuses)
|
snapshot.appendItems(items, toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
if let centerID = activity.userInfo?["centerID"] as? String ?? activity.userInfo?["topID"] as? String,
|
if let centerID = state.centerStatusID,
|
||||||
let index = existingStatuses.firstIndex(of: centerID),
|
let index = statusIDs.firstIndex(of: centerID),
|
||||||
let indexPath = self.dataSource.indexPath(for: items[index]) {
|
let indexPath = self.dataSource.indexPath(for: items[index]) {
|
||||||
// it sometimes takes multiple attempts to convert on the right scroll position
|
// it sometimes takes multiple attempts to convert on the right scroll position
|
||||||
// since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop
|
// since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop
|
||||||
|
@ -400,6 +399,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
disappearedAt = Date()
|
disappearedAt = Date()
|
||||||
|
saveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func refresh() {
|
@objc func refresh() {
|
||||||
|
|
|
@ -22,12 +22,15 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
|
|
||||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
||||||
home.title = homeTitle
|
home.title = homeTitle
|
||||||
|
home.persistsState = true
|
||||||
|
|
||||||
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||||
federated.title = federatedTitle
|
federated.title = federatedTitle
|
||||||
|
federated.persistsState = true
|
||||||
|
|
||||||
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
||||||
local.title = localTitle
|
local.title = localTitle
|
||||||
|
local.persistsState = true
|
||||||
|
|
||||||
super.init(pages: [
|
super.init(pages: [
|
||||||
(.home, "Home", home),
|
(.home, "Home", home),
|
||||||
|
@ -83,9 +86,6 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectPage(page, animated: false)
|
selectPage(page, animated: false)
|
||||||
// can't use currentIndex here because the view isn't loaded yet, and so the page wasn't actually updated by the selectPage call
|
|
||||||
let timelineVC = pageControllers[pages.firstIndex(of: page)!] as! TimelineViewController
|
|
||||||
timelineVC.restoreActivity(activity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filtersPressed() {
|
@objc private func filtersPressed() {
|
||||||
|
|
Loading…
Reference in New Issue