// // SuggestedProfilesViewController.swift // Tusker // // Created by Shadowfacts on 2/11/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class SuggestedProfilesViewController: UIViewController, CollectionViewController { private let mastodonController: MastodonController var collectionView: UICollectionView! private var layout: MultiColumnCollectionViewLayout! private var dataSource: UICollectionViewDiffableDataSource! private var state = State.unloaded init(mastodonController: MastodonController) { self.mastodonController = mastodonController super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = "Suggested Accounts" layout = MultiColumnCollectionViewLayout(numberOfColumns: 2, columnSpacing: 16, minimumColumnWidth: 320) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.delegate = self collectionView.dragDelegate = self collectionView.backgroundColor = .appGroupedBackground collectionView.allowsFocus = true view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) dataSource = createDataSource() } private func createDataSource() -> UICollectionViewDiffableDataSource { let accountCell = UICollectionView.CellRegistration(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in cell.delegate = self cell.updateUI(accountID: item.0, source: item.1) } let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .account(let id, let source): return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source)) } } let loadingView = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { view, elementKind, indexPath in view.indicator.startAnimating() } dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in if elementKind == UICollectionView.elementKindSectionHeader { return collectionView.dequeueConfiguredReusableSupplementary(using: loadingView, for: indexPath) } else { return nil } } return dataSource } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Task { await loadInitial() } } @MainActor private func loadInitial() async { guard case .unloaded = state else { return } state = .loading layout.showSectionHeader = true var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) await MainActor.run { dataSource.apply(snapshot) } do { let request = Client.getSuggestions(limit: 80) let (suggestions, _) = try await mastodonController.run(request) await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account)) layout.showSectionHeader = false var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }) await MainActor.run { dataSource.apply(snapshot) } state = .loaded } catch { state = .unloaded let config = ToastConfiguration(from: error, with: "Error Loading Suggested Accounts", in: self) { [weak self] toast in toast.dismissToast(animated: true) await self?.loadInitial() } showToast(configuration: config, animated: true) layout.showSectionHeader = false } } } extension SuggestedProfilesViewController { enum State { case unloaded case loading case loaded } } extension SuggestedProfilesViewController { enum Section { case accounts } enum Item: Hashable { case account(String, Suggestion.Source) } } extension SuggestedProfilesViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { if case .account(_, _) = dataSource.itemIdentifier(for: indexPath) { return true } else { return false } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath) else { return } selected(account: id) } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath), let cell = collectionView.cellForItem(at: indexPath) else { return nil } 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 SuggestedProfilesViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath), let account = mastodonController.persistentContainer.account(for: id) else { return [] } let provider = NSItemProvider(object: account.url as NSURL) let activity = UserActivityManager.showProfileActivity(id: id, accountID: mastodonController.accountInfo!.id) provider.registerObject(activity, visibility: .all) return [UIDragItem(itemProvider: provider)] } } extension SuggestedProfilesViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } } extension SuggestedProfilesViewController: MenuActionProvider { } extension SuggestedProfilesViewController: ToastableViewController { }