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 {
|
class StatusActionAccountListCollectionViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
|
private let statusID: String
|
||||||
|
private let actionType: StatusActionAccountListViewController.ActionType
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
/// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate.
|
/// 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 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.
|
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.
|
- 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
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
@ -38,6 +45,18 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
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
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
||||||
case .status:
|
case .status:
|
||||||
|
@ -51,7 +70,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
}
|
}
|
||||||
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
case .accounts:
|
case .accounts:
|
||||||
return NSCollectionLayoutSection.list(using: .init(appearance: .grouped), layoutEnvironment: environment)
|
return NSCollectionLayoutSection.list(using: accountsConfig, layoutEnvironment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
@ -70,12 +89,17 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item)
|
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
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .status(let id, let state):
|
case .status(let id, let state):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: 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
|
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { (headerView, collectionView, indexPath) in
|
||||||
|
@ -93,6 +117,12 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
|
if case .unloaded = state {
|
||||||
|
Task {
|
||||||
|
await loadAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addStatus(_ status: StatusMO, state: CollapseState) {
|
func addStatus(_ status: StatusMO, state: CollapseState) {
|
||||||
|
@ -104,12 +134,115 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
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()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts)
|
snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts)
|
||||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
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 {
|
extension StatusActionAccountListCollectionViewController {
|
||||||
|
@ -120,6 +253,16 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(String, CollapseState)
|
case status(String, CollapseState)
|
||||||
case account(String)
|
case account(String)
|
||||||
|
case loadingIndicator
|
||||||
|
|
||||||
|
var hideSeparators: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
@ -127,6 +270,8 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
return a == b
|
return a == b
|
||||||
case (.account(let a), .account(let b)):
|
case (.account(let a), .account(let b)):
|
||||||
return a == b
|
return a == b
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -140,12 +285,23 @@ extension StatusActionAccountListCollectionViewController {
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
hasher.combine(1)
|
hasher.combine(1)
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListCollectionViewController: UICollectionViewDelegate {
|
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) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case nil:
|
case nil:
|
||||||
|
@ -154,6 +310,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
|
||||||
selected(status: id, state: state.copy())
|
selected(status: id, state: state.copy())
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
selected(account: id)
|
selected(account: id)
|
||||||
|
case .loadingIndicator:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +329,8 @@ extension StatusActionAccountListCollectionViewController: UICollectionViewDeleg
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
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)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
case .loadingIndicator:
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,10 +95,12 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if case .unloaded = state {
|
||||||
Task {
|
Task {
|
||||||
await loadStatus()
|
await loadStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||||
guard let userInfo = notification.userInfo,
|
guard let userInfo = notification.userInfo,
|
||||||
|
@ -143,45 +145,13 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusLoaded(_ status: StatusMO) async {
|
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.addStatus(status, state: statusState)
|
||||||
vc.showInacurateCountWarning = showInacurateCountWarning
|
vc.showInacurateCountWarning = showInacurateCountWarning
|
||||||
state = .displaying(vc)
|
|
||||||
|
|
||||||
if let accountIDs {
|
if let accountIDs {
|
||||||
vc.addAccounts(accountIDs, animated: false)
|
vc.setAccounts(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)
|
|
||||||
}
|
}
|
||||||
|
state = .displaying(vc)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showStatusNotFound() {
|
private func showStatusNotFound() {
|
||||||
|
|
Loading…
Reference in New Issue