forked from shadowfacts/Tusker
Extract a bunch of timeline view controller stuff to separate protocol
This commit is contained in:
parent
d18a4b3c42
commit
a682c8f5cc
|
@ -35,6 +35,7 @@
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||||
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||||
|
@ -380,6 +381,7 @@
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
@ -1298,6 +1300,7 @@
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
|
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */,
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
|
@ -1900,6 +1903,7 @@
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
|
|
|
@ -14,20 +14,19 @@ import SwiftSoup
|
||||||
|
|
||||||
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
||||||
|
|
||||||
class TimelineViewController: UIViewController {
|
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController {
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var controller: TimelineLikeController<TimelineItem>!
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||||
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
private var collectionView: UICollectionView {
|
var collectionView: UICollectionView {
|
||||||
view as! UICollectionView
|
view as! UICollectionView
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
@ -61,12 +60,12 @@ class TimelineViewController: UIViewController {
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
// TODO: delegates
|
// TODO: delegates
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
// collectionView.dragDelegate = self
|
// collectionView.dragDelegate = self
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||||
Task {
|
Task {
|
||||||
await self.controller.loadNewer()
|
await self.controller.loadNewer()
|
||||||
|
@ -74,7 +73,7 @@ class TimelineViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
collectionView.refreshControl = refreshControl
|
collectionView.refreshControl = refreshControl
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -135,18 +134,26 @@ class TimelineViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController {
|
extension TimelineViewController {
|
||||||
enum Section: Hashable {
|
enum Section: TimelineLikeCollectionViewSection {
|
||||||
case header
|
case header
|
||||||
case statuses
|
case statuses
|
||||||
case footer
|
case footer
|
||||||
|
|
||||||
|
static var entries: Self { .statuses }
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: TimelineLikeCollectionViewItem {
|
||||||
|
typealias TimelineItem = String // status ID
|
||||||
|
|
||||||
case status(id: String, state: StatusState)
|
case status(id: String, state: StatusState)
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
case confirmLoadMore
|
case confirmLoadMore
|
||||||
// // TODO: remove local param from this
|
// // TODO: remove local param from this
|
||||||
// case publicTimelineDescription(local: Bool)
|
// case publicTimelineDescription(local: Bool)
|
||||||
|
|
||||||
|
static func fromTimelineItem(_ id: String) -> Self {
|
||||||
|
return .status(id: id, state: .unknown)
|
||||||
|
}
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||||
|
@ -188,7 +195,8 @@ extension TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: TimelineLikeControllerDelegate {
|
// MARK: - TimelineLikeControllerDelegate
|
||||||
|
extension TimelineViewController {
|
||||||
typealias TimelineItem = String // status ID
|
typealias TimelineItem = String // status ID
|
||||||
|
|
||||||
func loadInitial() async throws -> [TimelineItem] {
|
func loadInitial() async throws -> [TimelineItem] {
|
||||||
|
@ -255,120 +263,7 @@ extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func canLoadOlder() async -> Bool {
|
enum Error: TimelineLikeCollectionViewError {
|
||||||
if Preferences.shared.disableInfiniteScrolling {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
|
||||||
snapshot.appendSections([.footer])
|
|
||||||
}
|
|
||||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
for await _ in self.confirmLoadMore.values {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
fatalError("unreachable")
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
|
||||||
switch event {
|
|
||||||
case .addLoadingIndicator:
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
|
||||||
snapshot.appendSections([.footer])
|
|
||||||
}
|
|
||||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
snapshot.reconfigureItems([.confirmLoadMore])
|
|
||||||
} else {
|
|
||||||
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
|
||||||
}
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
case .removeLoadingIndicator:
|
|
||||||
let oldContentOffset = collectionView.contentOffset
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.deleteSections([.footer])
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
collectionView.contentOffset = oldContentOffset
|
|
||||||
|
|
||||||
case .loadAllError(let error, _):
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast: ToastView) in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadInitial()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
|
|
||||||
case .replaceAllItems(let ids, _):
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if snapshot.sectionIdentifiers.contains(.statuses) {
|
|
||||||
snapshot.deleteSections([.statuses])
|
|
||||||
}
|
|
||||||
snapshot.appendSections([.statuses])
|
|
||||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
case .loadNewerError(Error.allCaughtUp, _):
|
|
||||||
var config = ToastConfiguration(title: "You're all caught up")
|
|
||||||
config.edge = .top
|
|
||||||
config.dismissAutomaticallyAfter = 2
|
|
||||||
config.action = { (toast) in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
|
|
||||||
case .loadNewerError(let error, _):
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast: ToastView) in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadNewer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
|
|
||||||
case .prependItems(let ids, _):
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
let items = ids.map { Item.status(id: $0, state: .unknown) }
|
|
||||||
let first = snapshot.itemIdentifiers(inSection: .statuses).first
|
|
||||||
if let first {
|
|
||||||
snapshot.insertItems(items, beforeItem: first)
|
|
||||||
} else {
|
|
||||||
snapshot.appendItems(items, toSection: .statuses)
|
|
||||||
}
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
if let first,
|
|
||||||
let indexPath = dataSource.indexPath(for: first) {
|
|
||||||
// TODO: i can't tell if this actually works or not
|
|
||||||
// maintain the current position in the list (don't scroll to top)
|
|
||||||
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
case .loadOlderError(let error, _):
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast: ToastView) in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadOlder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
|
|
||||||
case .appendItems(let ids, _):
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
snapshot.deleteItems([.confirmLoadMore])
|
|
||||||
}
|
|
||||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Error: Swift.Error {
|
|
||||||
case noClient
|
case noClient
|
||||||
case noNewer
|
case noNewer
|
||||||
case noOlder
|
case noOlder
|
||||||
|
@ -391,6 +286,3 @@ extension TimelineViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
//
|
||||||
|
// TimelineLikeCollectionViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/24/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeControllerDelegate, ToastableViewController {
|
||||||
|
associatedtype Section: TimelineLikeCollectionViewSection
|
||||||
|
associatedtype Item: TimelineLikeCollectionViewItem where Item.TimelineItem == Self.TimelineItem
|
||||||
|
associatedtype Error: TimelineLikeCollectionViewError
|
||||||
|
|
||||||
|
// this needs to be an IUO because it can't be set until after the super init is called, so that self can be passed as the delegate param
|
||||||
|
var controller: TimelineLikeController<TimelineItem>! { get }
|
||||||
|
var confirmLoadMore: PassthroughSubject<Void, Never> { get }
|
||||||
|
|
||||||
|
var collectionView: UICollectionView { get }
|
||||||
|
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol TimelineLikeCollectionViewSection: Hashable {
|
||||||
|
static var entries: Self { get }
|
||||||
|
static var footer: Self { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol TimelineLikeCollectionViewItem: Hashable {
|
||||||
|
associatedtype TimelineItem
|
||||||
|
|
||||||
|
static var loadingIndicator: Self { get }
|
||||||
|
static var confirmLoadMore: Self { get }
|
||||||
|
|
||||||
|
static func fromTimelineItem(_ item: TimelineItem) -> Self
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: equatable might not be the best for this?
|
||||||
|
protocol TimelineLikeCollectionViewError: Error, Equatable {
|
||||||
|
static var allCaughtUp: Self { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineLikeCollectionViewController {
|
||||||
|
func canLoadOlder() async -> Bool {
|
||||||
|
if Preferences.shared.disableInfiniteScrolling {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||||
|
snapshot.appendSections([.footer])
|
||||||
|
}
|
||||||
|
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
for await _ in confirmLoadMore.values {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fatalError("unreachable")
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
||||||
|
switch event {
|
||||||
|
case .addLoadingIndicator:
|
||||||
|
await handleAddLoadingIndicator()
|
||||||
|
case .removeLoadingIndicator:
|
||||||
|
await handleRemoveLoadingIndicator()
|
||||||
|
case .loadAllError(let error, _):
|
||||||
|
await handleLoadAllError(error)
|
||||||
|
case .replaceAllItems(let items, _):
|
||||||
|
await handleReplaceAllItems(items)
|
||||||
|
case .loadNewerError(let error, _):
|
||||||
|
await handleLoadNewerError(error)
|
||||||
|
case .prependItems(let items, _):
|
||||||
|
await handlePrependItems(items)
|
||||||
|
case .loadOlderError(let error, _):
|
||||||
|
await handleLoadOlderError(error)
|
||||||
|
case .appendItems(let items, _):
|
||||||
|
await handleAppendItems(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAddLoadingIndicator() async {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||||
|
snapshot.appendSections([.footer])
|
||||||
|
}
|
||||||
|
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
snapshot.reconfigureItems([.confirmLoadMore])
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
||||||
|
}
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRemoveLoadingIndicator() async {
|
||||||
|
let oldContentOffset = collectionView.contentOffset
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.footer])
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
// prevent the collection view from scrolling as we remove the loading indicator and add the timeline items
|
||||||
|
collectionView.contentOffset = oldContentOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLoadAllError(_ error: Swift.Error) async {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleReplaceAllItems(_ timelineItems: [TimelineItem]) async {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.sectionIdentifiers.contains(.entries) {
|
||||||
|
snapshot.deleteSections([.entries])
|
||||||
|
}
|
||||||
|
snapshot.appendSections([.entries])
|
||||||
|
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLoadNewerError(_ error: Swift.Error) async {
|
||||||
|
var config: ToastConfiguration
|
||||||
|
if let error = error as? Self.Error,
|
||||||
|
error == .allCaughtUp {
|
||||||
|
config = ToastConfiguration(title: "You're all caught up")
|
||||||
|
config.edge = .top
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
config.action = { toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadNewer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePrependItems(_ timelineItems: [TimelineItem]) async {
|
||||||
|
let items = timelineItems.map { Item.fromTimelineItem($0) }
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
let first = snapshot.itemIdentifiers(inSection: .entries).first
|
||||||
|
if let first {
|
||||||
|
snapshot.insertItems(items, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(items, toSection: .entries)
|
||||||
|
}
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
if let first,
|
||||||
|
let indexPath = dataSource.indexPath(for: first) {
|
||||||
|
// TODO: i can't tell if this actually works or not
|
||||||
|
// maintain the current scroll position in the list (don't scroll to top)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLoadOlderError(_ error: Swift.Error) async {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadOlder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppendItems(_ timelineItems: [TimelineItem]) async {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
// TODO: this might not be necessary, isn't the confirm item removed separately?
|
||||||
|
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
|
}
|
||||||
|
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue