2020-06-24 20:40:45 +00:00
|
|
|
//
|
2023-02-05 19:23:29 +00:00
|
|
|
// TrendsViewController.swift
|
2020-06-24 20:40:45 +00:00
|
|
|
// Tusker
|
|
|
|
//
|
2023-02-05 19:23:29 +00:00
|
|
|
// Created by Shadowfacts on 2/5/23.
|
|
|
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
2020-06-24 20:40:45 +00:00
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
2022-07-01 02:26:28 +00:00
|
|
|
import Pachyderm
|
|
|
|
import SafariServices
|
2023-02-11 23:47:39 +00:00
|
|
|
import Combine
|
2020-06-24 20:40:45 +00:00
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
class TrendsViewController: UIViewController, CollectionViewController {
|
|
|
|
|
|
|
|
let mastodonController: MastodonController
|
2020-06-24 20:40:45 +00:00
|
|
|
|
2023-01-22 18:54:21 +00:00
|
|
|
var collectionView: UICollectionView!
|
2022-07-01 02:26:28 +00:00
|
|
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
|
|
|
2023-01-22 18:54:21 +00:00
|
|
|
private var loadTask: Task<Void, Never>?
|
2023-02-11 23:47:39 +00:00
|
|
|
private var trendingStatusesState = TrendingStatusesState.unloaded
|
|
|
|
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
|
2023-01-22 18:54:21 +00:00
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
private var isShowingTrends = false
|
|
|
|
private var shouldShowTrends: Bool {
|
2023-02-05 19:43:04 +00:00
|
|
|
mastodonController.instanceFeatures.trends && !Preferences.shared.hideTrends
|
2023-02-05 19:23:29 +00:00
|
|
|
}
|
|
|
|
|
2020-06-24 20:40:45 +00:00
|
|
|
init(mastodonController: MastodonController) {
|
|
|
|
self.mastodonController = mastodonController
|
|
|
|
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
title = "Trends"
|
2020-06-24 20:40:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
2023-02-05 19:23:29 +00:00
|
|
|
|
2020-06-24 20:40:45 +00:00
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
|
|
|
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
|
|
|
switch sectionIdentifier {
|
2023-02-11 23:32:37 +00:00
|
|
|
case .loadingIndicator:
|
|
|
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
|
|
listConfig.backgroundColor = .appGroupedBackground
|
|
|
|
listConfig.showsSeparators = false
|
|
|
|
return .list(using: listConfig, layoutEnvironment: environment)
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
case .trendingHashtags:
|
|
|
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
|
|
listConfig.headerMode = .supplementary
|
2023-02-10 23:19:00 +00:00
|
|
|
listConfig.footerMode = .supplementary
|
2023-02-03 04:02:11 +00:00
|
|
|
listConfig.backgroundColor = .appGroupedBackground
|
2022-07-01 02:26:28 +00:00
|
|
|
return .list(using: listConfig, layoutEnvironment: environment)
|
|
|
|
|
|
|
|
case .trendingLinks:
|
2023-01-22 22:17:59 +00:00
|
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
2022-07-01 02:26:28 +00:00
|
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
|
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
|
|
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
2023-02-05 19:41:10 +00:00
|
|
|
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
|
2022-07-01 02:26:28 +00:00
|
|
|
let section = NSCollectionLayoutSection(group: group)
|
2023-02-05 19:41:10 +00:00
|
|
|
section.orthogonalScrollingBehavior = .groupPaging
|
2022-07-01 02:26:28 +00:00
|
|
|
section.boundarySupplementaryItems = [
|
2023-02-10 23:19:00 +00:00
|
|
|
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),
|
2022-07-01 02:26:28 +00:00
|
|
|
]
|
2023-02-10 23:19:00 +00:00
|
|
|
section.contentInsets = .zero
|
2022-07-01 02:26:28 +00:00
|
|
|
return section
|
2023-01-23 22:10:26 +00:00
|
|
|
|
|
|
|
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])
|
2023-02-05 19:41:10 +00:00
|
|
|
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
|
2023-01-23 22:10:26 +00:00
|
|
|
let section = NSCollectionLayoutSection(group: group)
|
2023-02-05 19:41:10 +00:00
|
|
|
section.orthogonalScrollingBehavior = .groupPaging
|
2023-01-23 22:10:26 +00:00
|
|
|
section.boundarySupplementaryItems = [
|
2023-02-10 23:19:00 +00:00
|
|
|
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),
|
2023-01-23 22:10:26 +00:00
|
|
|
]
|
2023-02-10 23:19:00 +00:00
|
|
|
section.contentInsets = .zero
|
2023-01-23 22:10:26 +00:00
|
|
|
return section
|
2023-01-22 18:54:21 +00:00
|
|
|
|
|
|
|
case .trendingStatuses:
|
|
|
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
|
|
listConfig.headerMode = .supplementary
|
2023-02-03 04:02:11 +00:00
|
|
|
listConfig.backgroundColor = .appGroupedBackground
|
2023-02-11 23:47:39 +00:00
|
|
|
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
|
|
|
|
}
|
2023-01-23 22:10:26 +00:00
|
|
|
return .list(using: listConfig, layoutEnvironment: environment)
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
|
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
collectionView.delegate = self
|
|
|
|
collectionView.dragDelegate = self
|
2023-02-03 04:02:11 +00:00
|
|
|
collectionView.backgroundColor = .appGroupedBackground
|
2023-01-16 22:47:56 +00:00
|
|
|
collectionView.allowsFocus = true
|
2022-07-01 02:26:28 +00:00
|
|
|
view.addSubview(collectionView)
|
|
|
|
|
|
|
|
dataSource = createDataSource()
|
2020-12-14 03:37:37 +00:00
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
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
|
2023-02-07 02:26:42 +00:00
|
|
|
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
|
2022-07-01 02:26:28 +00:00
|
|
|
var config = UIListContentConfiguration.groupedHeader()
|
|
|
|
config.text = section.title
|
|
|
|
headerView.contentConfiguration = config
|
|
|
|
}
|
2023-02-11 23:47:39 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
|
2023-02-11 23:32:37 +00:00
|
|
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
|
|
|
cell.indicator.startAnimating()
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
|
|
|
|
cell.updateUI(hashtag: hashtag)
|
|
|
|
}
|
|
|
|
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
|
|
|
cell.updateUI(card: card)
|
|
|
|
}
|
2023-02-03 04:02:11 +00:00
|
|
|
let statusCell = UICollectionView.CellRegistration<TrendingStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
2023-01-22 18:54:21 +00:00
|
|
|
cell.delegate = self
|
|
|
|
// TODO: filter trends
|
|
|
|
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
|
|
|
}
|
2023-01-23 22:10:26 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-02-11 23:47:39 +00:00
|
|
|
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
|
|
|
|
cell.confirmLoadMore = self.confirmLoadMoreStatuses
|
|
|
|
cell.isLoading = isLoading
|
2023-02-10 23:19:00 +00:00
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
|
|
|
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
|
|
|
switch item {
|
2023-02-11 23:32:37 +00:00
|
|
|
case .loadingIndicator:
|
|
|
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
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)
|
|
|
|
|
2023-01-22 18:54:21 +00:00
|
|
|
case let .status(id, state):
|
|
|
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
2023-01-23 22:10:26 +00:00
|
|
|
|
|
|
|
case let .account(id, source):
|
|
|
|
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
2023-02-11 23:47:39 +00:00
|
|
|
|
|
|
|
case let .confirmLoadMoreStatuses(loading):
|
|
|
|
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
|
|
|
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
|
|
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
2023-02-10 23:19:00 +00:00
|
|
|
} else if elementKind == UICollectionView.elementKindSectionFooter {
|
|
|
|
return collectionView.dequeueConfiguredReusableSupplementary(using: moreCell, for: indexPath)
|
2022-07-01 02:26:28 +00:00
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dataSource
|
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
@MainActor
|
2023-02-05 19:23:29 +00:00
|
|
|
private func loadTrends() async {
|
|
|
|
guard isShowingTrends != shouldShowTrends else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
isShowingTrends = shouldShowTrends
|
|
|
|
guard shouldShowTrends else {
|
2022-07-01 02:26:28 +00:00
|
|
|
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
2023-02-11 23:32:37 +00:00
|
|
|
snapshot.appendSections([.loadingIndicator])
|
|
|
|
snapshot.appendItems([.loadingIndicator])
|
|
|
|
await apply(snapshot: snapshot)
|
|
|
|
|
|
|
|
snapshot = NSDiffableDataSourceSnapshot()
|
2023-01-22 18:54:21 +00:00
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
2023-01-23 22:10:26 +00:00
|
|
|
let hashtags = try? await mastodonController.run(hashtagsReq).0
|
2023-01-22 18:54:21 +00:00
|
|
|
|
2023-01-23 22:10:26 +00:00
|
|
|
if let hashtags {
|
2022-07-01 02:26:28 +00:00
|
|
|
snapshot.appendSections([.trendingHashtags])
|
|
|
|
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
|
|
|
|
}
|
2023-01-23 22:10:26 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-01-22 18:54:21 +00:00
|
|
|
|
|
|
|
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 {
|
2023-01-23 22:10:26 +00:00
|
|
|
if snapshot.sectionIdentifiers.contains(.profileSuggestions) {
|
|
|
|
snapshot.insertSections([.trendingLinks], beforeSection: .profileSuggestions)
|
|
|
|
} else {
|
|
|
|
snapshot.appendSections([.trendingLinks])
|
|
|
|
}
|
2023-01-22 18:54:21 +00:00
|
|
|
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 {
|
2023-01-23 22:10:26 +00:00
|
|
|
await apply(snapshot: snapshot)
|
2023-02-11 23:47:39 +00:00
|
|
|
if snapshot.sectionIdentifiers.contains(.trendingStatuses) {
|
|
|
|
self.trendingStatusesState = .loaded
|
|
|
|
} else {
|
|
|
|
self.trendingStatusesState = .unloaded
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 23:47:39 +00:00
|
|
|
@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
|
|
|
|
}
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
@objc private func preferencesChanged() {
|
2023-02-05 19:23:29 +00:00
|
|
|
if isShowingTrends != shouldShowTrends {
|
|
|
|
loadTask?.cancel()
|
|
|
|
loadTask = Task {
|
|
|
|
await loadTrends()
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-23 22:10:26 +00:00
|
|
|
|
2023-02-11 23:47:39 +00:00
|
|
|
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
|
2023-01-23 22:10:26 +00:00
|
|
|
await Task { @MainActor in
|
2023-02-11 23:47:39 +00:00
|
|
|
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
2023-01-23 22:10:26 +00:00
|
|
|
}.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)
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 23:47:39 +00:00
|
|
|
extension TrendsViewController {
|
|
|
|
enum TrendingStatusesState {
|
|
|
|
case unloaded
|
|
|
|
case loaded
|
|
|
|
case loadingOlder
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController {
|
2022-07-01 02:26:28 +00:00
|
|
|
enum Section {
|
2023-02-11 23:32:37 +00:00
|
|
|
case loadingIndicator
|
2022-07-01 02:26:28 +00:00
|
|
|
case trendingHashtags
|
|
|
|
case trendingLinks
|
|
|
|
case profileSuggestions
|
2023-01-22 18:54:21 +00:00
|
|
|
case trendingStatuses
|
2022-07-01 02:26:28 +00:00
|
|
|
|
2023-02-11 23:32:37 +00:00
|
|
|
var title: String? {
|
2022-07-01 02:26:28 +00:00
|
|
|
switch self {
|
2023-02-11 23:32:37 +00:00
|
|
|
case .loadingIndicator:
|
|
|
|
return nil
|
2022-07-01 02:26:28 +00:00
|
|
|
case .trendingHashtags:
|
|
|
|
return "Trending Hashtags"
|
|
|
|
case .trendingLinks:
|
|
|
|
return "Trending Links"
|
|
|
|
case .trendingStatuses:
|
2023-01-22 18:54:21 +00:00
|
|
|
return "Trending Posts"
|
2022-07-01 02:26:28 +00:00
|
|
|
case .profileSuggestions:
|
|
|
|
return "Suggested Accounts"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
enum Item: Equatable, Hashable {
|
2023-02-11 23:32:37 +00:00
|
|
|
case loadingIndicator
|
2023-01-22 18:54:21 +00:00
|
|
|
case status(String, CollapseState)
|
2022-07-01 02:26:28 +00:00
|
|
|
case tag(Hashtag)
|
|
|
|
case link(Card)
|
2023-01-23 22:10:26 +00:00
|
|
|
case account(String, Suggestion.Source)
|
2023-02-11 23:47:39 +00:00
|
|
|
case confirmLoadMoreStatuses(Bool)
|
2022-07-01 02:26:28 +00:00
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
2022-07-01 02:26:28 +00:00
|
|
|
switch (lhs, rhs) {
|
2023-02-11 23:32:37 +00:00
|
|
|
case (.loadingIndicator, .loadingIndicator):
|
|
|
|
return true
|
2023-01-22 18:54:21 +00:00
|
|
|
case let (.status(a, _), .status(b, _)):
|
2022-07-01 02:26:28 +00:00
|
|
|
return a == b
|
|
|
|
case let (.tag(a), .tag(b)):
|
|
|
|
return a == b
|
|
|
|
case let (.link(a), .link(b)):
|
|
|
|
return a.url == b.url
|
2023-01-23 22:10:26 +00:00
|
|
|
case let (.account(a, _), .account(b, _)):
|
|
|
|
return a == b
|
2023-02-11 23:47:39 +00:00
|
|
|
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
|
|
|
|
return a == b
|
2022-07-01 02:26:28 +00:00
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
|
|
switch self {
|
2023-02-11 23:32:37 +00:00
|
|
|
case .loadingIndicator:
|
|
|
|
hasher.combine("loadingIndicator")
|
2023-01-22 18:54:21 +00:00
|
|
|
case let .status(id, _):
|
2022-07-01 02:26:28 +00:00
|
|
|
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)
|
2023-01-23 22:10:26 +00:00
|
|
|
case let .account(id, _):
|
|
|
|
hasher.combine("account")
|
|
|
|
hasher.combine(id)
|
2023-02-11 23:47:39 +00:00
|
|
|
case let .confirmLoadMoreStatuses(loading):
|
|
|
|
hasher.combine("confirmLoadMoreStatuses")
|
|
|
|
hasher.combine(loading)
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-11 23:32:37 +00:00
|
|
|
|
|
|
|
var shouldSelect: Bool {
|
|
|
|
switch self {
|
2023-02-11 23:47:39 +00:00
|
|
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
2023-02-11 23:32:37 +00:00
|
|
|
return false
|
|
|
|
default:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2023-02-11 23:47:39 +00:00
|
|
|
|
|
|
|
var hideListSeparators: Bool {
|
|
|
|
switch self {
|
|
|
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: UICollectionViewDelegate {
|
2023-02-11 23:47:39 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 23:32:37 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
|
|
|
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
|
|
|
|
}
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch item {
|
2023-02-11 23:47:39 +00:00
|
|
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
2023-02-11 23:32:37 +00:00
|
|
|
return
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-01-22 18:54:21 +00:00
|
|
|
case let .status(id, state):
|
|
|
|
selected(status: id, state: state.copy())
|
2023-01-23 22:10:26 +00:00
|
|
|
|
|
|
|
case let .account(id, _):
|
|
|
|
selected(account: id)
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-22 17:06:51 +00:00
|
|
|
@available(iOS, obsoleted: 16.0)
|
2022-07-01 02:26:28 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch item {
|
2023-02-11 23:47:39 +00:00
|
|
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
2023-02-11 23:32:37 +00:00
|
|
|
return nil
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
case let .tag(hashtag):
|
|
|
|
return UIContextMenuConfiguration(identifier: nil) {
|
|
|
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
|
|
|
} actionProvider: { (_) in
|
2022-11-30 03:41:36 +00:00
|
|
|
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self.collectionView.cellForItem(at: indexPath))))
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case let .link(card):
|
|
|
|
guard let url = URL(card.url) else {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-11 15:21:09 +00:00
|
|
|
let cell = collectionView.cellForItem(at: indexPath)!
|
2022-07-01 02:26:28 +00:00
|
|
|
return UIContextMenuConfiguration {
|
2023-01-16 16:24:42 +00:00
|
|
|
let vc = SFSafariViewController(url: url)
|
|
|
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
|
|
|
return vc
|
2022-07-01 02:26:28 +00:00
|
|
|
} actionProvider: { _ in
|
2023-02-11 15:21:09 +00:00
|
|
|
UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
|
2023-01-22 19:12:05 +00:00
|
|
|
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)))
|
|
|
|
}
|
2023-01-23 22:10:26 +00:00
|
|
|
|
|
|
|
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)))
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-22 17:06:51 +00:00
|
|
|
|
|
|
|
// 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, *)
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-01-22 19:12:05 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
|
|
|
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
|
|
|
}
|
|
|
|
|
2023-01-22 17:06:51 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, highlightPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? {
|
|
|
|
switch dataSource.itemIdentifier(for: indexPath) {
|
2023-01-23 22:10:26 +00:00
|
|
|
case .link(_), .account(_, _):
|
2023-01-22 17:06:51 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: UICollectionViewDragDelegate {
|
2022-07-01 02:26:28 +00:00
|
|
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
switch item {
|
2023-02-11 23:47:39 +00:00
|
|
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
2023-02-11 23:32:37 +00:00
|
|
|
return []
|
|
|
|
|
2022-07-01 02:26:28 +00:00
|
|
|
case let .tag(hashtag):
|
2022-07-09 15:45:27 +00:00
|
|
|
guard let url = URL(hashtag.url) else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
let provider = NSItemProvider(object: url as NSURL)
|
2022-07-01 02:26:28 +00:00
|
|
|
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))]
|
|
|
|
|
2023-01-22 18:54:21 +00:00
|
|
|
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
|
2023-01-23 22:10:26 +00:00
|
|
|
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
|
2023-01-22 18:54:21 +00:00
|
|
|
provider.registerObject(activity, visibility: .all)
|
|
|
|
return [UIDragItem(itemProvider: provider)]
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: TuskerNavigationDelegate {
|
2022-10-31 20:27:13 +00:00
|
|
|
var apiController: MastodonController! { mastodonController }
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: ToastableViewController {
|
2022-07-01 02:26:28 +00:00
|
|
|
}
|
2020-06-24 20:40:45 +00:00
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: MenuActionProvider {
|
2020-06-24 20:40:45 +00:00
|
|
|
}
|
2023-01-22 18:54:21 +00:00
|
|
|
|
2023-02-05 19:23:29 +00:00
|
|
|
extension TrendsViewController: StatusCollectionViewCellDelegate {
|
2023-01-22 18:54:21 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|