From ecadb83c6ddbe702dba03d162927c90d5f2218e4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 11 Feb 2023 18:47:39 -0500 Subject: [PATCH] Add infinite scrolling to trending statuses See #355 --- .../Pachyderm/Sources/Pachyderm/Client.swift | 13 +- .../TrendingHashtagsViewController.swift | 2 +- .../Explore/TrendingLinksViewController.swift | 13 +- .../Explore/TrendsViewController.swift | 138 +++++++++++++++--- 4 files changed, 134 insertions(+), 32 deletions(-) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Client.swift b/Packages/Pachyderm/Sources/Pachyderm/Client.swift index 10926b2b..b78f35db 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Client.swift @@ -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) } diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index d7bd54c6..2888676b 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -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() } diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index 578cc48b..fc05f89c 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -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 diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 6116763e..0709e6d9 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -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! private var loadTask: Task? + private var trendingStatusesState = TrendingStatusesState.unloaded + private let confirmLoadMoreStatuses = PassthroughSubject() 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(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 { 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(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 { [unowned self] cell, indexPath, isLoading in + cell.confirmLoadMore = self.confirmLoadMoreStatuses + cell.isLoading = isLoading } let dataSource = UICollectionViewDiffableDataSource(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,9 +268,60 @@ 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 { loadTask?.cancel() @@ -261,9 +331,9 @@ class TrendsViewController: UIViewController, CollectionViewController { } } - private func apply(snapshot: NSDiffableDataSourceSnapshot) async { + private func apply(snapshot: NSDiffableDataSourceSnapshot, 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):