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) return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters)
} }
public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> { public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> {
let parameters: [Parameter] var parameters: [Parameter] = []
if let limit = limit { if let limit {
parameters = ["limit" => limit] parameters.append("limit" => limit)
} else { }
parameters = [] if let offset {
parameters.append("offset" => offset)
} }
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters) return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
} }

View File

@ -163,7 +163,7 @@ class TrendingHashtagsViewController: UIViewController {
} catch { } catch {
await dataSource.apply(origSnapshot) 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) toast.dismissToast(animated: true)
await self?.loadOlder() await self?.loadOlder()
} }

View File

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

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import Combine
class TrendsViewController: UIViewController, CollectionViewController { class TrendsViewController: UIViewController, CollectionViewController {
@ -18,6 +19,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loadTask: Task<Void, Never>? private var loadTask: Task<Void, Never>?
private var trendingStatusesState = TrendingStatusesState.unloaded
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
private var isShowingTrends = false private var isShowingTrends = false
private var shouldShowTrends: Bool { private var shouldShowTrends: Bool {
@ -89,6 +92,15 @@ class TrendsViewController: UIViewController, CollectionViewController {
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped) var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary listConfig.headerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground 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) return .list(using: listConfig, layoutEnvironment: environment)
} }
} }
@ -112,6 +124,19 @@ class TrendsViewController: UIViewController, CollectionViewController {
config.text = section.title config.text = section.title
headerView.contentConfiguration = config 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 let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating() cell.indicator.startAnimating()
@ -131,18 +156,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1) cell.updateUI(accountID: item.0, source: item.1)
} }
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
supplementaryView.delegate = self cell.confirmLoadMore = self.confirmLoadMoreStatuses
switch self.dataSource.sectionIdentifier(for: indexPath.section) { cell.isLoading = isLoading
case nil, .loadingIndicator, .trendingStatuses:
fatalError()
case .trendingHashtags:
supplementaryView.updateUI(.hashtags)
case .trendingLinks:
supplementaryView.updateUI(.links)
case .profileSuggestions:
supplementaryView.updateUI(.profileSuggestions)
}
} }
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
@ -161,6 +177,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
case let .account(id, source): case let .account(id, source):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (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 dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
@ -249,8 +268,59 @@ class TrendsViewController: UIViewController, CollectionViewController {
if !Task.isCancelled { if !Task.isCancelled {
await apply(snapshot: snapshot) 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() { @objc private func preferencesChanged() {
if isShowingTrends != shouldShowTrends { 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 await Task { @MainActor in
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value }.value
} }
@ -286,6 +356,14 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
} }
extension TrendsViewController {
enum TrendingStatusesState {
case unloaded
case loaded
case loadingOlder
}
}
extension TrendsViewController { extension TrendsViewController {
enum Section { enum Section {
case loadingIndicator case loadingIndicator
@ -315,6 +393,7 @@ extension TrendsViewController {
case tag(Hashtag) case tag(Hashtag)
case link(Card) case link(Card)
case account(String, Suggestion.Source) case account(String, Suggestion.Source)
case confirmLoadMoreStatuses(Bool)
static func == (lhs: Item, rhs: Item) -> Bool { static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
@ -328,6 +407,8 @@ extension TrendsViewController {
return a.url == b.url return a.url == b.url
case let (.account(a, _), .account(b, _)): case let (.account(a, _), .account(b, _)):
return a == b return a == b
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
return a == b
default: default:
return false return false
} }
@ -349,21 +430,42 @@ extension TrendsViewController {
case let .account(id, _): case let .account(id, _):
hasher.combine("account") hasher.combine("account")
hasher.combine(id) hasher.combine(id)
case let .confirmLoadMoreStatuses(loading):
hasher.combine("confirmLoadMoreStatuses")
hasher.combine(loading)
} }
} }
var shouldSelect: Bool { var shouldSelect: Bool {
switch self { switch self {
case .loadingIndicator: case .loadingIndicator, .confirmLoadMoreStatuses(_):
return false return false
default: default:
return true return true
} }
} }
var hideListSeparators: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return true
default:
return false
}
}
} }
} }
extension TrendsViewController: UICollectionViewDelegate { 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 { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
} }
@ -373,7 +475,7 @@ extension TrendsViewController: UICollectionViewDelegate {
return return
} }
switch item { switch item {
case .loadingIndicator: case .loadingIndicator, .confirmLoadMoreStatuses(_):
return return
case let .tag(hashtag): case let .tag(hashtag):
@ -399,7 +501,7 @@ extension TrendsViewController: UICollectionViewDelegate {
} }
switch item { switch item {
case .loadingIndicator: case .loadingIndicator, .confirmLoadMoreStatuses(_):
return nil return nil
case let .tag(hashtag): case let .tag(hashtag):
@ -487,7 +589,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return [] return []
} }
switch item { switch item {
case .loadingIndicator: case .loadingIndicator, .confirmLoadMoreStatuses(_):
return [] return []
case let .tag(hashtag): case let .tag(hashtag):