// // StatusActionAccountListViewController.swift // Tusker // // Created by Shadowfacts on 9/5/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class StatusActionAccountListViewController: UIViewController { private let mastodonController: MastodonController private let actionType: ActionType private let statusID: String private let statusState: CollapseState private var accountIDs: [String]? /// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate. var showInacurateCountWarning = false private var collectionView: UICollectionView { view as! UICollectionView } private var dataSource: UICollectionViewDiffableDataSource! /** Creates a new view controller showing the accounts that performed the given action on the given status. - Parameter actionType The action that this VC is for. - Parameter statusID The ID of the status to show. - Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts. - Parameter mastodonController The `MastodonController` instance this view controller uses. */ init(actionType: ActionType, statusID: String, statusState: CollapseState, accountIDs: [String]?, mastodonController: MastodonController) { self.mastodonController = mastodonController self.actionType = actionType self.statusID = statusID self.statusState = statusState self.accountIDs = accountIDs super.init(nibName: nil, bundle: nil) switch actionType { case .favorite: title = NSLocalizedString("Favorited By", comment: "status favorited by accounts list title") case .reblog: title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title") } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in switch dataSource.sectionIdentifier(for: sectionIndex)! { case .status: var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.footerMode = self.showInacurateCountWarning ? .supplementary : .none config.leadingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() } config.trailingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions() } return NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment) case .accounts: return NSCollectionLayoutSection.list(using: .init(appearance: .grouped), layoutEnvironment: environment) } } view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self dataSource = createDataSource() } private func createDataSource() -> UICollectionViewDiffableDataSource { let statusCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, _ in cell.delegate = self cell.updateUI(statusID: self.statusID, state: self.statusState, filterResult: .allow, precomputedContent: nil) } let accountCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in cell.delegate = self cell.updateUI(accountID: item) } let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .status: return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: ()) case .account(let id): return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id) } } let sectionHeaderCell = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionFooter) { (headerView, collectionView, indexPath) in var config = headerView.defaultContentConfiguration() config.text = NSLocalizedString("Favorite and reblog counts for posts originating from instances other than your own may not be accurate.", comment: "shown on lists of status total actions") headerView.contentConfiguration = config } dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath) } return dataSource } override func viewDidLoad() { super.viewDidLoad() var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.status, .accounts]) snapshot.appendItems([.status], toSection: .status) if let accountIDs { snapshot.appendItems(accountIDs.map { .account($0) }, toSection: .accounts) } dataSource.apply(snapshot, animatingDifferences: false) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: true) } if accountIDs == nil { Task { await loadAccounts() } } } private func loadAccounts() 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() } } accountIDs = accounts.map(\.id) var snapshot = dataSource.snapshot() snapshot.appendItems(accounts.map { .account($0.id) }, toSection: .accounts) dataSource.apply(snapshot, animatingDifferences: true) {} } catch { 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) } } } extension StatusActionAccountListViewController { enum ActionType { case favorite, reblog } } extension StatusActionAccountListViewController { enum Section { case status case accounts } enum Item: Hashable { case status case account(String) } } extension StatusActionAccountListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { switch dataSource.itemIdentifier(for: indexPath) { case nil: return case .status: selected(status: statusID, state: statusState.copy()) case .account(let id): selected(account: id) } } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard let item = dataSource.itemIdentifier(for: indexPath), let cell = collectionView.cellForItem(at: indexPath) else { return nil } switch item { case .status: return (cell as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() case .account(let id): return UIContextMenuConfiguration { ProfileViewController(accountID: id, mastodonController: self.mastodonController) } actionProvider: { _ in UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell))) } } } func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) } } extension StatusActionAccountListViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let currentAccountID = mastodonController.accountInfo?.id, let item = dataSource.itemIdentifier(for: indexPath) else { return [] } let provider: NSItemProvider switch item { case .status: guard let status = mastodonController.persistentContainer.status(for: statusID) else { return [] } provider = NSItemProvider(object: status.url! as NSURL) let activity = UserActivityManager.showConversationActivity(mainStatusID: statusID, accountID: currentAccountID) activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) case .account(let id): guard let account = mastodonController.persistentContainer.account(for: id) else { return [] } provider = NSItemProvider(object: account.url as NSURL) let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID) activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) } return [UIDragItem(itemProvider: provider)] } } extension StatusActionAccountListViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } } extension StatusActionAccountListViewController: MenuActionProvider { } extension StatusActionAccountListViewController: StatusCollectionViewCellDelegate { func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) { if let indexPath = collectionView.indexPath(for: cell) { var snapshot = dataSource.snapshot() snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!]) dataSource.apply(snapshot, animatingDifferences: animated, completion: completion) } } func statusCellShowFiltered(_ cell: StatusCollectionViewCell) { fatalError() } } extension StatusActionAccountListViewController: StatusBarTappableViewController { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { collectionView.scrollToTop() return .stop } }