forked from shadowfacts/Tusker
330 lines
13 KiB
Swift
330 lines
13 KiB
Swift
//
|
|
// SearchViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 6/24/20.
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import SafariServices
|
|
import WebURLFoundationExtras
|
|
|
|
class SearchViewController: UIViewController {
|
|
|
|
weak var mastodonController: MastodonController!
|
|
|
|
private var collectionView: UICollectionView!
|
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
|
|
var resultsController: SearchResultsViewController!
|
|
var searchController: UISearchController!
|
|
|
|
var searchControllerStatusOnAppearance: Bool? = nil
|
|
|
|
init(mastodonController: MastodonController) {
|
|
self.mastodonController = mastodonController
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
title = NSLocalizedString("Explore", comment: "explore tab title")
|
|
}
|
|
|
|
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 .trendingHashtags:
|
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
listConfig.headerMode = .supplementary
|
|
return .list(using: listConfig, layoutEnvironment: environment)
|
|
|
|
case .trendingLinks:
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
// todo: i really wish i could just say the height is automatic and let autolayout figure out what it needs to be
|
|
// using .estimated(whatever) constrains the height to exactly whatever
|
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
|
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
|
let section = NSCollectionLayoutSection(group: group)
|
|
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
|
section.boundarySupplementaryItems = [
|
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
|
]
|
|
return section
|
|
|
|
default:
|
|
fatalError("unimplemented")
|
|
}
|
|
}
|
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
collectionView.delegate = self
|
|
collectionView.dragDelegate = self
|
|
collectionView.backgroundColor = .secondarySystemBackground
|
|
view.addSubview(collectionView)
|
|
|
|
dataSource = createDataSource()
|
|
|
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
|
resultsController.exploreNavigationController = self.navigationController
|
|
searchController = UISearchController(searchResultsController: resultsController)
|
|
searchController.obscuresBackgroundDuringPresentation = true
|
|
searchController.searchBar.autocapitalizationType = .none
|
|
searchController.searchBar.delegate = resultsController
|
|
searchController.hidesNavigationBarDuringPresentation = false
|
|
definesPresentationContext = true
|
|
|
|
navigationItem.searchController = searchController
|
|
navigationItem.hidesSearchBarWhenScrolling = false
|
|
if #available(iOS 16.0, *) {
|
|
navigationItem.preferredSearchBarPlacement = .stacked
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
Task(priority: .userInitiated) {
|
|
if (try? await mastodonController.getOwnInstance()) != nil {
|
|
await applySnapshot()
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
|
// does not cause it to automatically become active once it becomes visible
|
|
// see FB7814561
|
|
if let active = searchControllerStatusOnAppearance {
|
|
searchController.isActive = active
|
|
searchControllerStatusOnAppearance = 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.snapshot().sectionIdentifiers[indexPath.section]
|
|
var config = UIListContentConfiguration.groupedHeader()
|
|
config.text = section.title
|
|
headerView.contentConfiguration = config
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
|
switch 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)
|
|
|
|
default:
|
|
fatalError("todo")
|
|
}
|
|
}
|
|
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
|
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return dataSource
|
|
}
|
|
|
|
@MainActor
|
|
private func applySnapshot() async {
|
|
guard mastodonController.instanceFeatures.trends,
|
|
!Preferences.shared.hideDiscover else {
|
|
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
|
return
|
|
}
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
|
|
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
|
async let hashtags = try? mastodonController.run(hashtagsReq).0
|
|
let linksReq = Client.getTrendingLinks(limit: 10)
|
|
async let links = try? mastodonController.run(linksReq).0
|
|
|
|
if let hashtags = await hashtags {
|
|
snapshot.appendSections([.trendingHashtags])
|
|
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
|
|
}
|
|
|
|
if let links = await links {
|
|
snapshot.appendSections([.trendingLinks])
|
|
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
|
|
}
|
|
|
|
await dataSource.apply(snapshot)
|
|
}
|
|
|
|
@objc private func preferencesChanged() {
|
|
Task {
|
|
await applySnapshot()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension SearchViewController {
|
|
enum Section {
|
|
case trendingHashtags
|
|
case trendingLinks
|
|
case trendingStatuses
|
|
case profileSuggestions
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .trendingHashtags:
|
|
return "Trending Hashtags"
|
|
case .trendingLinks:
|
|
return "Trending Links"
|
|
case .trendingStatuses:
|
|
return "Trending Statuses"
|
|
case .profileSuggestions:
|
|
return "Suggested Accounts"
|
|
}
|
|
}
|
|
}
|
|
enum Item: Equatable, Hashable {
|
|
case status(String)
|
|
case tag(Hashtag)
|
|
case link(Card)
|
|
|
|
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
|
switch (lhs, rhs) {
|
|
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
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
switch self {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SearchViewController: UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
return
|
|
}
|
|
switch item {
|
|
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)
|
|
}
|
|
|
|
default:
|
|
fatalError("todo")
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
return nil
|
|
}
|
|
|
|
switch item {
|
|
case let .tag(hashtag):
|
|
return UIContextMenuConfiguration(identifier: nil) {
|
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
|
} actionProvider: { (_) in
|
|
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
|
|
}
|
|
|
|
case let .link(card):
|
|
guard let url = URL(card.url) else {
|
|
return nil
|
|
}
|
|
return UIContextMenuConfiguration {
|
|
SFSafariViewController(url: url)
|
|
} actionProvider: { _ in
|
|
UIMenu(children: self.actionsForTrendingLink(card: card))
|
|
}
|
|
|
|
default:
|
|
fatalError("todo")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SearchViewController: UICollectionViewDragDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
return []
|
|
}
|
|
switch item {
|
|
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))]
|
|
|
|
default:
|
|
fatalError("todo")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SearchViewController: TuskerNavigationDelegate {
|
|
var apiController: MastodonController! { mastodonController }
|
|
}
|
|
|
|
extension SearchViewController: ToastableViewController {
|
|
}
|
|
|
|
extension SearchViewController: MenuActionProvider {
|
|
}
|