Tusker/Tusker/Screens/Explore/TrendsViewController.swift

691 lines
31 KiB
Swift

//
// TrendsViewController.swift
// Tusker
//
// Created by Shadowfacts on 2/5/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import SafariServices
import Combine
#if os(visionOS)
import SwiftUI
#endif
class TrendsViewController: UIViewController, CollectionViewController {
let mastodonController: MastodonController
var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loadTask: Task<Void, Never>?
private var trendingStatusesState = TrendingStatusesState.unloaded
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
private var isShowingTrends = false
private var shouldShowTrends: Bool {
mastodonController.instanceFeatures.trends && !Preferences.shared.hideTrends
}
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
title = "Trends"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
switch sectionIdentifier {
case .loadingIndicator:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.backgroundColor = .appGroupedBackground
listConfig.showsSeparators = false
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingHashtags:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
listConfig.footerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingLinks:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
section.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading),
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
]
section.contentInsets = .zero
return section
case .profileSuggestions:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(250))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(250))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
section.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading),
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
]
section.contentInsets = .zero
return section
case .trendingStatuses:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
listConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
var config = sectionConfig
if let item = dataSource.itemIdentifier(for: indexPath),
item.hideListSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
return config
}
return .list(using: listConfig, layoutEnvironment: environment)
}
}
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true
view.addSubview(collectionView)
dataSource = createDataSource()
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
var config = UIListContentConfiguration.groupedHeader()
config.text = section.title
headerView.contentConfiguration = config
}
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in
supplementaryView.delegate = self
switch self.dataSource.sectionIdentifier(for: indexPath.section) {
case nil, .loadingIndicator, .trendingStatuses:
fatalError()
case .trendingHashtags:
supplementaryView.updateUI(.hashtags)
case .trendingLinks:
supplementaryView.updateUI(.links)
case .profileSuggestions:
supplementaryView.updateUI(.profileSuggestions)
}
}
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
cell.updateUI(hashtag: hashtag)
}
#if os(visionOS)
let trendingLinkCell = UICollectionView.CellRegistration<UICollectionViewCell, Card> { cell, indexPath, card in
cell.contentConfiguration = UIHostingConfiguration(content: {
TrendingLinkCardView(card: card)
})
}
#else
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
cell.updateUI(card: card)
}
#endif
let statusCell = UICollectionView.CellRegistration<TrendingStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
cell.delegate = self
// TODO: filter trends
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
}
#if os(visionOS)
let accountCell = UICollectionView.CellRegistration<UICollectionViewCell, (String, Suggestion.Source)> { [unowned self] cell, indexPath, item in
if let account = self.mastodonController.persistentContainer.account(for: item.0) {
cell.contentConfiguration = UIHostingConfiguration(content: {
SuggestedProfileCardView(account: account)
})
} else {
cell.contentConfiguration = nil
}
}
#else
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { [unowned self] cell, indexPath, item in
cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1)
}
#endif
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
cell.confirmLoadMore = self.confirmLoadMoreStatuses
cell.isLoading = isLoading
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
case let .link(card):
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
case let .status(id, state):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
case let .account(id, source):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
case let .confirmLoadMoreStatuses(loading):
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
}
}
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else if elementKind == UICollectionView.elementKindSectionFooter {
return collectionView.dequeueConfiguredReusableSupplementary(using: moreCell, for: indexPath)
} else {
return nil
}
}
return dataSource
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
clearSelectionOnAppear(animated: animated)
if loadTask == nil {
loadTask = Task(priority: .userInitiated) {
if (try? await mastodonController.getOwnInstance()) != nil {
await loadTrends()
}
}
}
}
@MainActor
private func loadTrends() async {
guard isShowingTrends != shouldShowTrends else {
return
}
isShowingTrends = shouldShowTrends
guard shouldShowTrends else {
await dataSource.apply(NSDiffableDataSourceSnapshot())
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
await apply(snapshot: snapshot)
snapshot = NSDiffableDataSourceSnapshot()
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
let hashtags = try? await mastodonController.run(hashtagsReq).0
if let hashtags {
snapshot.appendSections([.trendingHashtags])
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
}
if mastodonController.instanceFeatures.profileSuggestions {
let req = Client.getSuggestions(limit: 10)
let suggestions = try? await mastodonController.run(req).0
if let suggestions {
snapshot.appendSections([.profileSuggestions])
await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account))
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }, toSection: .profileSuggestions)
}
}
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
let linksReq = Client.getTrendingLinks(limit: 10)
async let links = try? mastodonController.run(linksReq).0
let statusesReq = Client.getTrendingStatuses(limit: 10)
async let statuses = try? mastodonController.run(statusesReq).0
if let links = await links {
if snapshot.sectionIdentifiers.contains(.profileSuggestions) {
snapshot.insertSections([.trendingLinks], beforeSection: .profileSuggestions)
} else {
snapshot.appendSections([.trendingLinks])
}
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
}
if let statuses = await statuses {
await mastodonController.persistentContainer.addAll(statuses: statuses)
snapshot.appendSections([.trendingStatuses])
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
}
}
if !Task.isCancelled {
await apply(snapshot: snapshot)
if snapshot.sectionIdentifiers.contains(.trendingStatuses) {
self.trendingStatusesState = .loaded
} else {
self.trendingStatusesState = .unloaded
}
}
}
@MainActor
private func loadOlderStatuses() async {
guard case .loaded = trendingStatusesState else {
return
}
trendingStatusesState = .loadingOlder
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMoreStatuses(false)], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
for await _ in confirmLoadMoreStatuses.values {
break
}
snapshot.deleteItems([.confirmLoadMoreStatuses(false)])
snapshot.appendItems([.confirmLoadMoreStatuses(true)], toSection: .trendingStatuses)
await apply(snapshot: snapshot, animatingDifferences: false)
} else {
snapshot.appendItems([.loadingIndicator], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
}
do {
let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count)
let (statuses, _) = try await mastodonController.run(request)
await mastodonController.persistentContainer.addAll(statuses: statuses)
var snapshot = origSnapshot
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
await apply(snapshot: snapshot)
} catch {
await apply(snapshot: origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading More Trending Statuses", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlderStatuses()
}
showToast(configuration: config, animated: true)
}
trendingStatusesState = .loaded
}
@objc private func preferencesChanged() {
if isShowingTrends != shouldShowTrends {
loadTask?.cancel()
loadTask = Task {
await loadTrends()
}
}
}
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
await Task { @MainActor in
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value
}
@MainActor
private func removeProfileSuggestion(accountID: String) async {
let req = Suggestion.remove(accountID: accountID)
do {
_ = try await mastodonController.run(req)
var snapshot = dataSource.snapshot()
// the source here doesn't matter, since it's ignored by the equatable and hashable impls
snapshot.deleteItems([.account(accountID, .global)])
await apply(snapshot: snapshot)
} catch {
let config = ToastConfiguration(from: error, with: "Error Removing Suggestion", in: self) { [unowned self] toast in
toast.dismissToast(animated: true)
_ = await self.removeProfileSuggestion(accountID: accountID)
}
self.showToast(configuration: config, animated: true)
}
}
}
extension TrendsViewController {
enum TrendingStatusesState {
case unloaded
case loaded
case loadingOlder
}
}
extension TrendsViewController {
enum Section {
case loadingIndicator
case trendingHashtags
case trendingLinks
case profileSuggestions
case trendingStatuses
var title: String? {
switch self {
case .loadingIndicator:
return nil
case .trendingHashtags:
return "Trending Hashtags"
case .trendingLinks:
return "Trending Links"
case .trendingStatuses:
return "Trending Posts"
case .profileSuggestions:
return "Suggested Accounts"
}
}
}
enum Item: Equatable, Hashable {
case loadingIndicator
case status(String, CollapseState)
case tag(Hashtag)
case link(Card)
case account(String, Suggestion.Source)
case confirmLoadMoreStatuses(Bool)
static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.loadingIndicator, .loadingIndicator):
return true
case let (.status(a, _), .status(b, _)):
return a == b
case let (.tag(a), .tag(b)):
return a == b
case let (.link(a), .link(b)):
return a.url == b.url
case let (.account(a, _), .account(b, _)):
return a == b
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
return a == b
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .loadingIndicator:
hasher.combine("loadingIndicator")
case let .status(id, _):
hasher.combine("status")
hasher.combine(id)
case let .tag(tag):
hasher.combine("tag")
hasher.combine(tag.name)
case let .link(card):
hasher.combine("link")
hasher.combine(card.url)
case let .account(id, _):
hasher.combine("account")
hasher.combine(id)
case let .confirmLoadMoreStatuses(loading):
hasher.combine("confirmLoadMoreStatuses")
hasher.combine(loading)
}
}
var shouldSelect: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return false
default:
return true
}
}
var hideListSeparators: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return true
default:
return false
}
}
}
}
extension TrendsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if case .trendingStatuses = dataSource.sectionIdentifier(for: indexPath.section),
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await self.loadOlderStatuses()
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return
case let .tag(hashtag):
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
case let .link(card):
if let url = URL(card.url) {
selected(url: url)
}
case let .status(id, state):
selected(status: id, state: state.copy())
case let .account(id, _):
selected(account: id)
}
}
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return nil
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return nil
case let .tag(hashtag):
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self.collectionView.cellForItem(at: indexPath))))
}
case let .link(card):
guard let url = URL(card.url) else {
return nil
}
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration {
let vc = SFSafariViewController(url: url)
#if !os(visionOS)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
#endif
return vc
} actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
}
case let .status(id, state):
guard let status = mastodonController.persistentContainer.status(for: id) else {
return nil
}
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration {
ConversationViewController(for: id, state: state.copy(), mastodonController: self.mastodonController)
} actionProvider: { _ in
UIMenu(children: self.actionsForStatus(status, source: .view(cell)))
}
case let .account(id, _):
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration {
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
} actionProvider: { _ in
let dismiss = UIAction(title: "Remove Suggestion", image: UIImage(systemName: "trash"), attributes: .destructive) { [unowned self] _ in
Task {
await self.removeProfileSuggestion(accountID: id)
}
}
return UIMenu(children: [UIMenu(options: .displayInline, children: [dismiss])] + self.actionsForProfile(accountID: id, source: .view(cell)))
}
}
}
// implementing the highlightPreviewForItemAt method seems to prevent the old, single IndexPath variant of this method from being called on iOS 16
@available(iOS 16.0, visionOS 1.0, *)
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
guard indexPaths.count == 1 else {
return nil
}
return self.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPaths[0], point: point)
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, highlightPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? {
switch dataSource.itemIdentifier(for: indexPath) {
case .link(_), .account(_, _):
guard let cell = collectionView.cellForItem(at: indexPath) else {
return nil
}
let params = UIPreviewParameters()
params.visiblePath = UIBezierPath(roundedRect: cell.bounds, cornerRadius: cell.contentView.layer.cornerRadius)
return UITargetedPreview(view: cell, parameters: params)
default:
return nil
}
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, dismissalPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? {
return self.collectionView(collectionView, contextMenuConfiguration: configuration, highlightPreviewForItemAt: indexPath)
}
#if os(visionOS)
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
return self.collectionView(collectionView, shouldSelectItemAt: indexPath)
}
#endif
}
extension TrendsViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return []
case let .tag(hashtag):
guard let url = URL(hashtag.url) else {
return []
}
let provider = NSItemProvider(object: url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
}
return [UIDragItem(itemProvider: provider)]
case let .link(card):
guard let url = URL(card.url) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
case let .status(id, _):
guard let status = mastodonController.persistentContainer.status(for: id),
let url = status.url else {
return []
}
let provider = NSItemProvider(object: url as NSURL)
let activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
case let .account(id, _):
guard 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)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}
}
}
extension TrendsViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController }
}
extension TrendsViewController: ToastableViewController {
}
extension TrendsViewController: MenuActionProvider {
}
extension TrendsViewController: 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) {
// TODO: filtering
}
}