// // SuggestedProfilesViewController.swift // Tusker // // Created by Shadowfacts on 2/11/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class SuggestedProfilesViewController: UIViewController, CollectionViewController { weak var mastodonController: MastodonController! var collectionView: UICollectionView! 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" let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in switch dataSource.sectionIdentifier(for: sectionIndex) { case nil: fatalError() case .loadingIndicator: var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.backgroundColor = .appGroupedBackground config.showsSeparators = false return .list(using: config, layoutEnvironment: environment) case .accounts: let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280)) let item = NSCollectionLayoutItem(layoutSize: size) let item2 = NSCollectionLayoutItem(layoutSize: size) let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2]) group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) group.interItemSpacing = .fixed(16) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 16 section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0) return section } } 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 loadingCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in cell.indicator.startAnimating() } 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) } return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .loadingIndicator: return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ()) case .account(let id, let source): return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source)) } } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Task { await loadInitial() } } @MainActor private func loadInitial() async { guard case .unloaded = state else { return } state = .loading var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.loadingIndicator]) snapshot.appendItems([.loadingIndicator]) await 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)) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }) await 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) } } } extension SuggestedProfilesViewController { enum State { case unloaded case loading case loaded } } extension SuggestedProfilesViewController { enum Section { case loadingIndicator case accounts } enum Item: Hashable { case loadingIndicator 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 { }