diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index d3aac6930f..4fa3fa791e 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -412,7 +412,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro // I'm not sure whether this should move into TimelineLikeController/TimelineLikeCollectionViewController let (_, presentItems) = await (loadNewerAndEndRefreshing(), try? loadInitial()) if let presentItems, !presentItems.isEmpty { - insertPresentItemsIfNecessary(presentItems) + insertPresentItemsAndShowJumpToast(presentItems) } } } @@ -430,13 +430,13 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0))) } } else { - insertPresentItemsIfNecessary(presentItems) + insertPresentItemsAndShowJumpToast(presentItems) } } } - private func insertPresentItemsIfNecessary(_ presentItems: [String]) { - let snapshot = dataSource.snapshot() + private func insertPresentItemsAndShowJumpToast(_ presentItems: [String]) { + var snapshot = dataSource.snapshot() guard snapshot.indexOfSection(.statuses) != nil else { return } @@ -444,13 +444,38 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro if case .status(id: let firstID, _, _) = currentItems.first, // if there's no overlap between presentItems and the existing items in the data source, prompt the user !presentItems.contains(firstID) { + let applySnapshotBeforeScrolling: Bool - // create a new snapshot to reset the timeline to the "present" state - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.statuses]) - snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses) + // remove any existing gap, if there is one + if let index = currentItems.lastIndex(of: .gap) { + snapshot.deleteItems(Array(currentItems[index...])) + + let statusesSection = snapshot.indexOfSection(.statuses)! + if collectionView.indexPathsForVisibleItems.contains(IndexPath(row: index, section: statusesSection)) { + // the gap cell is on screen + applySnapshotBeforeScrolling = false + } else if let topMostVisibleCell = collectionView.indexPathsForVisibleItems.first(where: { $0.section == statusesSection }), + index < topMostVisibleCell.row { + // the gap cell is above the top, so applying the snapshot would remove the currently-viewed statuses + applySnapshotBeforeScrolling = false + } else { + // the gap cell is below the bottom of the screen + applySnapshotBeforeScrolling = true + } + } else { + // there is no existing gap + applySnapshotBeforeScrolling = true + } + snapshot.insertItems([.gap], beforeItem: currentItems.first!) + snapshot.insertItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, beforeItem: .gap) - var config = ToastConfiguration(title: "Jump to present") + if applySnapshotBeforeScrolling { + let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min()! + let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)! + applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem) + } + + var config = ToastConfiguration(title: "Jump to Present") config.edge = .top config.systemImageName = "arrow.up" config.dismissAutomaticallyAfter = 4 @@ -467,19 +492,24 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro } else { origItemAtTop = nil } - - self.dataSource.apply(snapshot, animatingDifferences: true) { + + // when the user explicitly taps Jump to Present, we drop all the old items to let infinite scrolling work properly when they scroll back down + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses) + // don't animate the snapshot change, the scrolling animation will paper over the switch + self.dataSource.apply(snapshot, animatingDifferences: false) { self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true) - var config = ToastConfiguration(title: "Go back") + var config = ToastConfiguration(title: "Go Back") config.edge = .top config.systemImageName = "arrow.down" - config.dismissAutomaticallyAfter = 4 + config.dismissAutomaticallyAfter = 2 config.action = { [unowned self] toast in toast.dismissToast(animated: true) // todo: it would be nice if we could animate this, but that doesn't work with the screen-position-maintaining stuff if let (item, offset) = origItemAtTop { - self.applySnapshot(snapshot, maintainingScreenPosition: offset, ofItem: item) + self.applySnapshot(origSnapshot, maintainingScreenPosition: offset, ofItem: item) } else { self.dataSource.apply(origSnapshot, animatingDifferences: false) } @@ -516,12 +546,17 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro // firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area var cur = indexPathOfItemToMaintain var amountScrolledUp: CGFloat = 0 + var first = true while true { if cur.row <= 0 { break } if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain), cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop { + // if we're breaking from the loop at the first iteration, we need to make sure to still call scrollToItem for the current row + if first { + self.collectionView.scrollToItem(at: cur, at: .top, animated: false) + } break } cur = IndexPath(row: cur.row - 1, section: cur.section) @@ -529,6 +564,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro self.collectionView.layoutIfNeeded() let attrs = self.collectionView.layoutAttributesForItem(at: cur)! amountScrolledUp += attrs.size.height + first = false } self.collectionView.contentOffset.y += amountScrolledUp self.collectionView.contentOffset.y -= offsetFromTop