forked from shadowfacts/Tusker
Add pagination to status actions account list
This commit is contained in:
parent
4211806b5f
commit
bf739b9f41
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue