// // TrendingStatusesViewController.swift // Tusker // // Created by Shadowfacts on 4/1/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class TrendingStatusesViewController: UIViewController { weak var mastodonController: MastodonController! private var collectionView: UICollectionView { view as! UICollectionView } private var dataSource: UICollectionViewDiffableDataSource! init(mastodonController: MastodonController) { self.mastodonController = mastodonController super.init(nibName: nil, bundle: nil) title = NSLocalizedString("Trending Posts", comment: "trending posts screen title") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { var config = UICollectionLayoutListConfiguration(appearance: .plain) config.leadingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() } config.trailingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions() } config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return sectionSeparatorConfiguration } var config = sectionSeparatorConfiguration if item.hideSeparators { config.topSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden } if case .status(_, _) = item { config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets } return config } let layout = UICollectionViewCompositionalLayout.list(using: config) 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, item in cell.delegate = self // TODO: filter these cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil) } let loadingCell = UICollectionView.CellRegistration { cell, _, _ in cell.indicator.startAnimating() } return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .status(id: let id, state: let state): return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state)) case .loadingIndicator: return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ()) } } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.statuses]) snapshot.appendItems([.loadingIndicator]) dataSource.apply(snapshot, animatingDifferences: false) Task { await loadTrendingStatuses() } } private func loadTrendingStatuses() async { let statuses: [Status] do { statuses = try await mastodonController.run(Client.getTrendingStatuses()).0 } catch { let snapshot = NSDiffableDataSourceSnapshot() await dataSource.apply(snapshot) let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in toast.dismissToast(animated: true) await self.loadTrendingStatuses() } showToast(configuration: config, animated: true) return } await mastodonController.persistentContainer.addAll(statuses: statuses) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.statuses]) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }) await dataSource.apply(snapshot) } } extension TrendingStatusesViewController { enum Section { case statuses } enum Item: Hashable { case status(id: String, state: CollapseState) case loadingIndicator static func ==(lhs: Item, rhs: Item) -> Bool { switch (lhs, rhs) { case (.status(id: let a, state: _), .status(id: let b, state: _)): return a == b case (.loadingIndicator, .loadingIndicator): return true default: return false } } func hash(into hasher: inout Hasher) { switch self { case .status(id: let id, state: _): hasher.combine(0) hasher.combine(id) case .loadingIndicator: hasher.combine(1) } } var hideSeparators: Bool { if case .loadingIndicator = self { return true } else { return false } } var isSelectable: Bool { if case .status(id: _, state: _) = self { return true } else { return false } } } } extension TrendingStatusesViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { return dataSource.itemIdentifier(for: indexPath)?.isSelectable ?? false } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard case .status(id: let id, state: let state) = dataSource.itemIdentifier(for: indexPath) else { return } selected(status: id, state: state.copy()) } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() } func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) } } extension TrendingStatusesViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? [] } } extension TrendingStatusesViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } } extension TrendingStatusesViewController: ToastableViewController { } extension TrendingStatusesViewController: MenuActionProvider { } extension TrendingStatusesViewController: 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 TrendingStatusesViewController: StatusBarTappableViewController { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { collectionView.scrollToTop() return .stop } }