Add infinite scrolling to trending statuses

See #355
This commit is contained in:
Shadowfacts 2023-02-11 18:47:39 -05:00
parent 205bdffebd
commit ecadb83c6d
4 changed files with 134 additions and 32 deletions

View File

@ -439,12 +439,13 @@ public class Client {
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters)
}
public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> {
let parameters: [Parameter]
if let limit = limit {
parameters = ["limit" => limit]
} else {
parameters = []
public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> {
var parameters: [Parameter] = []
if let limit {
parameters.append("limit" => limit)
}
if let offset {
parameters.append("offset" => offset)
}
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
}

View File

@ -163,7 +163,7 @@ class TrendingHashtagsViewController: UIViewController {
} catch {
await dataSource.apply(origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading Older Tags", in: self) { [weak self] toast in
let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlder()
}

View File

@ -42,7 +42,7 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
case nil:
fatalError()
case .loadingIndicator, .confirmLoadMore:
case .loadingIndicator:
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.showsSeparators = false
@ -155,8 +155,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendSections([.confirmLoadMore])
snapshot.appendItems([.confirmLoadMore(false)], toSection: .confirmLoadMore)
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator)
await dataSource.apply(snapshot)
for await _ in confirmLoadMore.values {
@ -164,11 +164,11 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
}
snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)], toSection: .confirmLoadMore)
snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator)
await dataSource.apply(snapshot, animatingDifferences: false)
} else {
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator], toSection: .confirmLoadMore)
snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator)
await dataSource.apply(snapshot)
}
@ -180,7 +180,7 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
await dataSource.apply(snapshot)
} catch {
await dataSource.apply(origSnapshot)
let config = ToastConfiguration(from: error, with: "Erorr Loading Older Links", in: self) { [weak self] toast in
let config = ToastConfiguration(from: error, with: "Erorr Loading More Links", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlder()
}
@ -203,7 +203,6 @@ extension TrendingLinksViewController {
enum Section {
case loadingIndicator
case links
case confirmLoadMore
}
enum Item: Hashable {
case loadingIndicator

View File

@ -9,6 +9,7 @@
import UIKit
import Pachyderm
import SafariServices
import Combine
class TrendsViewController: UIViewController, CollectionViewController {
@ -18,6 +19,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loadTask: Task<Void, Never>?
private var trendingStatusesState = TrendingStatusesState.unloaded
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
private var isShowingTrends = false
private var shouldShowTrends: Bool {
@ -89,6 +92,15 @@ class TrendsViewController: UIViewController, CollectionViewController {
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
listConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
var config = sectionConfig
if let item = dataSource.itemIdentifier(for: indexPath),
item.hideListSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
return config
}
return .list(using: listConfig, layoutEnvironment: environment)
}
}
@ -112,6 +124,19 @@ class TrendsViewController: UIViewController, CollectionViewController {
config.text = section.title
headerView.contentConfiguration = config
}
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in
supplementaryView.delegate = self
switch self.dataSource.sectionIdentifier(for: indexPath.section) {
case nil, .loadingIndicator, .trendingStatuses:
fatalError()
case .trendingHashtags:
supplementaryView.updateUI(.hashtags)
case .trendingLinks:
supplementaryView.updateUI(.links)
case .profileSuggestions:
supplementaryView.updateUI(.profileSuggestions)
}
}
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
@ -131,18 +156,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1)
}
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in
supplementaryView.delegate = self
switch self.dataSource.sectionIdentifier(for: indexPath.section) {
case nil, .loadingIndicator, .trendingStatuses:
fatalError()
case .trendingHashtags:
supplementaryView.updateUI(.hashtags)
case .trendingLinks:
supplementaryView.updateUI(.links)
case .profileSuggestions:
supplementaryView.updateUI(.profileSuggestions)
}
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
cell.confirmLoadMore = self.confirmLoadMoreStatuses
cell.isLoading = isLoading
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
@ -161,6 +177,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
case let .account(id, source):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
case let .confirmLoadMoreStatuses(loading):
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
}
}
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
@ -249,8 +268,59 @@ class TrendsViewController: UIViewController, CollectionViewController {
if !Task.isCancelled {
await apply(snapshot: snapshot)
if snapshot.sectionIdentifiers.contains(.trendingStatuses) {
self.trendingStatusesState = .loaded
} else {
self.trendingStatusesState = .unloaded
}
}
}
@MainActor
private func loadOlderStatuses() async {
guard case .loaded = trendingStatusesState else {
return
}
trendingStatusesState = .loadingOlder
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMoreStatuses(false)], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
for await _ in confirmLoadMoreStatuses.values {
break
}
snapshot.deleteItems([.confirmLoadMoreStatuses(false)])
snapshot.appendItems([.confirmLoadMoreStatuses(true)], toSection: .trendingStatuses)
await apply(snapshot: snapshot, animatingDifferences: false)
} else {
snapshot.appendItems([.loadingIndicator], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
}
do {
let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count)
let (statuses, _) = try await mastodonController.run(request)
await mastodonController.persistentContainer.addAll(statuses: statuses)
var snapshot = origSnapshot
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
await apply(snapshot: snapshot)
} catch {
await apply(snapshot: origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading More Trending Statuses", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlderStatuses()
}
showToast(configuration: config, animated: true)
}
trendingStatusesState = .loaded
}
@objc private func preferencesChanged() {
if isShowingTrends != shouldShowTrends {
@ -261,9 +331,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
}
}
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
await Task { @MainActor in
self.dataSource.apply(snapshot)
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value
}
@ -286,6 +356,14 @@ class TrendsViewController: UIViewController, CollectionViewController {
}
}
extension TrendsViewController {
enum TrendingStatusesState {
case unloaded
case loaded
case loadingOlder
}
}
extension TrendsViewController {
enum Section {
case loadingIndicator
@ -315,6 +393,7 @@ extension TrendsViewController {
case tag(Hashtag)
case link(Card)
case account(String, Suggestion.Source)
case confirmLoadMoreStatuses(Bool)
static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
@ -328,6 +407,8 @@ extension TrendsViewController {
return a.url == b.url
case let (.account(a, _), .account(b, _)):
return a == b
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
return a == b
default:
return false
}
@ -349,21 +430,42 @@ extension TrendsViewController {
case let .account(id, _):
hasher.combine("account")
hasher.combine(id)
case let .confirmLoadMoreStatuses(loading):
hasher.combine("confirmLoadMoreStatuses")
hasher.combine(loading)
}
}
var shouldSelect: Bool {
switch self {
case .loadingIndicator:
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return false
default:
return true
}
}
var hideListSeparators: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return true
default:
return false
}
}
}
}
extension TrendsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if case .trendingStatuses = dataSource.sectionIdentifier(for: indexPath.section),
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await self.loadOlderStatuses()
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
}
@ -373,7 +475,7 @@ extension TrendsViewController: UICollectionViewDelegate {
return
}
switch item {
case .loadingIndicator:
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return
case let .tag(hashtag):
@ -399,7 +501,7 @@ extension TrendsViewController: UICollectionViewDelegate {
}
switch item {
case .loadingIndicator:
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return nil
case let .tag(hashtag):
@ -487,7 +589,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return []
}
switch item {
case .loadingIndicator:
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return []
case let .tag(hashtag):