// // ProfileDirectoryViewController.swift // Tusker // // Created by Shadowfacts on 2/6/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class ProfileDirectoryViewController: UIViewController { private let mastodonController: MastodonController private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var scope: Scope = .everywhere private var order: DirectoryOrder = .active 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 = NSLocalizedString("Profile Directory", comment: "profile directory title") let filterItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: nil) filterItem.accessibilityLabel = "Filter" navigationItem.rightBarButtonItem = filterItem updateFilterMenu() let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in let itemHeight = NSCollectionLayoutDimension.absolute(200) let itemWidth: NSCollectionLayoutDimension if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass { itemWidth = .fractionalWidth(1) } else { itemWidth = .absolute((layoutEnvironment.container.contentSize.width - 16 - 8 * 2) / 2) } let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemHeight) let item = NSCollectionLayoutItem(layoutSize: itemSize) let itemB = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemHeight) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, itemB]) group.interItemSpacing = .flexible(16) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 16 section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) return section }) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .appSecondaryBackground collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell") collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true view.addSubview(collectionView) dataSource = createDataSource() updateProfiles() } private func createDataSource() -> UICollectionViewDiffableDataSource { let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) in guard case let .account(account) = item else { fatalError() } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "featuredProfileCell", for: indexPath) as! FeaturedProfileCollectionViewCell cell.updateUI(account: account) return cell } return dataSource } private func updateFilterMenu() { let scopeMenu = UIMenu(options: .displayInline, children: [ UIAction(title: "Everywhere", subtitle: "Users from the whole network", image: UIImage(systemName: "globe"), state: scope == .everywhere ? .on : .off, handler: { [unowned self] _ in self.scope = .everywhere self.updateFilterMenu() self.updateProfiles() }), UIAction(title: mastodonController.accountInfo!.instanceURL.host!, subtitle: "Only users from your instance", image: UIImage(systemName: "house"), state: scope == .instance ? .on : .off, handler: { [unowned self] _ in self.scope = .instance self.updateFilterMenu() self.updateProfiles() }), ]) let orderMenu = UIMenu(options: .displayInline, children: DirectoryOrder.allCases.map { order in UIAction(title: order.title, subtitle: order.subtitle, image: nil, state: self.order == order ? .on : .off) { [unowned self] _ in self.order = order self.updateFilterMenu() self.updateProfiles() } }) navigationItem.rightBarButtonItem!.menu = UIMenu(children: [ scopeMenu, orderMenu, ]) } private func updateProfiles() { let scope = self.scope let order = self.order let local = scope == .instance let request = Client.getFeaturedProfiles(local: local, order: order) mastodonController.run(request) { (response) in guard case let .success(accounts, _) = response, self.scope == scope, self.order == order else { return } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.featuredProfiles]) snapshot.appendItems(accounts.map { .account($0) }) DispatchQueue.main.async { self.dataSource.apply(snapshot) } } } } extension ProfileDirectoryViewController { enum Section { case featuredProfiles } enum Item: Hashable { case account(Account) func hash(into hasher: inout Hasher) { guard case let .account(account) = self else { return } hasher.combine(account.id) } } } extension ProfileDirectoryViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } } extension ProfileDirectoryViewController: ToastableViewController { } extension ProfileDirectoryViewController: MenuActionProvider { } extension ProfileDirectoryViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath), case let .account(account) = item else { return } show(ProfileViewController(accountID: account.id, mastodonController: mastodonController), sender: nil) } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard let item = dataSource.itemIdentifier(for: indexPath), case let .account(account) = item else { return nil } return UIContextMenuConfiguration(identifier: nil) { return ProfileViewController(accountID: account.id, mastodonController: self.mastodonController) } actionProvider: { (_) in let actions = self.actionsForProfile(accountID: account.id, source: .view(self.collectionView.cellForItem(at: indexPath))) return UIMenu(children: actions) } } func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { if let viewController = animator.previewViewController { animator.preferredCommitStyle = .pop animator.addCompletion { self.show(viewController, sender: nil) } } } } extension ProfileDirectoryViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let item = dataSource.itemIdentifier(for: indexPath), case let .account(account) = item, let currentAccountID = mastodonController.accountInfo?.id else { return [] } let 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 ProfileDirectoryViewController { enum Scope: CaseIterable { case instance, everywhere } } private extension DirectoryOrder { var title: String { switch self { case .active: return "Active" case .new: return "New" } } var subtitle: String { switch self { case .active: return "Users who have posted lately" case .new: return "Recently joined users" } } }