Add pagination to status actions account list

This commit is contained in:
Shadowfacts 2023-01-18 14:56:13 -05:00
parent 4211806b5f
commit bf739b9f41
2 changed files with 172 additions and 40 deletions

View File

@ -11,6 +11,8 @@ import Pachyderm
class StatusActionAccountListCollectionViewController: UIViewController, CollectionViewController {
private let statusID: String
private let actionType: StatusActionAccountListViewController.ActionType
private let mastodonController: MastodonController
/// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate.
@ -21,12 +23,17 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state: State = .unloaded
private var older: RequestRange?
/**
Creates a new view controller showing the accounts that performed the given action on the given status.
- Parameter mastodonController The `MastodonController` instance this view controller uses.
*/
init(mastodonController: MastodonController) {
init(statusID: String, actionType: StatusActionAccountListViewController.ActionType, mastodonController: MastodonController) {
self.statusID = statusID
self.actionType = actionType
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
@ -38,6 +45,18 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
}
override func loadView() {
var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
accountsConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
return sectionConfig
}
var config = sectionConfig
if item.hideSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
return config
}
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
switch dataSource.sectionIdentifier(for: sectionIndex)! {
case .status:
@ -51,7 +70,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
}
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
case .accounts:
return NSCollectionLayoutSection.list(using: .init(appearance: .grouped), layoutEnvironment: environment)
return NSCollectionLayoutSection.list(using: accountsConfig, layoutEnvironment: environment)
}
}
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
@ -70,12 +89,17 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
cell.delegate = self
cell.updateUI(accountID: item)
}
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
cell.indicator.startAnimating()
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .status(let id, let state):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
case .account(let id):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
}
}
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { (headerView, collectionView, indexPath) in
@ -93,6 +117,12 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
super.viewWillAppear(animated)
clearSelectionOnAppear(animated: animated)
if case .unloaded = state {
Task {
await loadAccounts()
}
}
}
func addStatus(_ status: StatusMO, state: CollapseState) {
@ -104,12 +134,115 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
dataSource.apply(snapshot, animatingDifferences: false)
}
func addAccounts(_ accountIDs: [String], animated: Bool) {
func setAccounts(_ accountIDs: [String], animated: Bool) {
guard case .unloaded = state else {
return
}
var snapshot = dataSource.snapshot()
snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts)
dataSource.apply(snapshot, animatingDifferences: animated)
self.state = .loaded
}
private func request(for range: RequestRange) -> Request<[Account]> {
switch actionType {
case .favorite:
return Status.getFavourites(statusID, range: range)
case .reblog:
return Status.getReblogs(statusID, range: range)
}
}
func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
await Task { @MainActor in
self.dataSource.apply(snapshot)
}.value
}
@MainActor
private func loadAccounts() async {
guard case .unloaded = state else {
return
}
self.state = .loadingInitial
var snapshot = dataSource.snapshot()
snapshot.appendItems([.loadingIndicator], toSection: .accounts)
await apply(snapshot: snapshot)
do {
let (accounts, pagination) = try await mastodonController.run(request(for: .default))
await mastodonController.persistentContainer.addAll(accounts: accounts)
guard case .loadingInitial = self.state else {
return
}
self.state = .loaded
self.older = pagination?.older
var snapshot = dataSource.snapshot()
snapshot.deleteItems([.loadingIndicator])
snapshot.appendItems(accounts.map { .account($0.id) }, toSection: .accounts)
await apply(snapshot: snapshot)
} catch {
self.state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { toast in
toast.dismissToast(animated: true)
await self.loadAccounts()
}
self.showToast(configuration: config, animated: true)
}
}
@MainActor
private func loadOlder() async {
guard case .loaded = state,
let older else {
return
}
self.state = .loadingOlder
var snapshot = self.dataSource.snapshot()
snapshot.appendItems([.loadingIndicator], toSection: .accounts)
await apply(snapshot: snapshot)
do {
try! await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
let (accounts, pagination) = try await mastodonController.run(request(for: older))
await mastodonController.persistentContainer.addAll(accounts: accounts)
guard case .loadingOlder = self.state else {
return
}
self.state = .loaded
self.older = pagination?.older
var snapshot = dataSource.snapshot()
snapshot.deleteItems([.loadingIndicator])
snapshot.appendItems(accounts.map { .account($0.id) }, toSection: .accounts)
await apply(snapshot: snapshot)
} catch {
self.state = .loaded
let config = ToastConfiguration(from: error, with: "Error Loading More", in: self) { [unowned self] toast in
toast.dismissToast(animated: true)
await self.loadOlder()
}
self.showToast(configuration: config, animated: true)
}
}
}
extension StatusActionAccountListCollectionViewController {
enum State {
case unloaded
case loadingInitial
case loaded
case loadingOlder
}
}
extension StatusActionAccountListCollectionViewController {
@ -120,6 +253,16 @@ extension StatusActionAccountListCollectionViewController {
enum Item: Hashable {
case status(String, CollapseState)
case account(String)
case loadingIndicator
var hideSeparators: Bool {
switch self {
case .loadingIndicator:
return true
default:
return false
}
}
static func ==(lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
@ -127,6 +270,8 @@ extension StatusActionAccountListCollectionViewController {
return a == b
case (.account(let a), .account(let b)):
return a == b
case (.loadingIndicator, .loadingIndicator):
return true
default:
return false
}
@ -140,12 +285,23 @@ extension StatusActionAccountListCollectionViewController {
case .account(let id):
hasher.combine(1)
hasher.combine(id)
case .loadingIndicator:
hasher.combine(2)
}
}
}
}
extension StatusActionAccountListCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.section == collectionView.numberOfSections - 1,
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await self.loadOlder()
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
switch dataSource.itemIdentifier(for: indexPath) {
case nil:
@ -154,6 +310,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
selected(status: id, state: state.copy())
case .account(let id):
selected(account: id)
case .loadingIndicator:
return
}
}
@ -171,6 +329,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
} actionProvider: { _ in
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
}
case .loadingIndicator:
return nil
}
}
@ -203,6 +363,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDragD
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
case .loadingIndicator:
return []
}
return [UIDragItem(itemProvider: provider)]
}

View File

@ -95,8 +95,10 @@ class StatusActionAccountListViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task {
await loadStatus()
if case .unloaded = state {
Task {
await loadStatus()
}
}
}
@ -143,45 +145,13 @@ class StatusActionAccountListViewController: UIViewController {
}
private func statusLoaded(_ status: StatusMO) async {
let vc = StatusActionAccountListCollectionViewController(mastodonController: mastodonController)
let vc = StatusActionAccountListCollectionViewController(statusID: statusID, actionType: actionType, mastodonController: mastodonController)
vc.addStatus(status, state: statusState)
vc.showInacurateCountWarning = showInacurateCountWarning
state = .displaying(vc)
if let accountIDs {
vc.addAccounts(accountIDs, animated: false)
} else {
await loadAccounts(list: vc)
}
}
private func loadAccounts(list: StatusActionAccountListCollectionViewController) async {
let request: Request<[Account]>
switch actionType {
case .favorite:
request = Status.getFavourites(statusID)
case .reblog:
request = Status.getReblogs(statusID)
}
do {
// TODO: pagination
let (accounts, _) = try await mastodonController.run(request)
await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addAll(accounts: accounts) {
continuation.resume()
}
}
list.addAccounts(accounts.map(\.id), animated: true)
} catch {
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { toast in
toast.dismissToast(animated: true)
await self.loadAccounts(list: list)
}
self.showToast(configuration: config, animated: true)
vc.setAccounts(accountIDs, animated: false)
}
state = .displaying(vc)
}
private func showStatusNotFound() {