forked from shadowfacts/Tusker
438 lines
18 KiB
Swift
438 lines
18 KiB
Swift
//
|
|
// ExploreViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 12/14/19.
|
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Combine
|
|
import Pachyderm
|
|
|
|
class ExploreViewController: EnhancedTableViewController {
|
|
|
|
weak var mastodonController: MastodonController!
|
|
|
|
var dataSource: DataSource!
|
|
|
|
var resultsController: SearchResultsViewController!
|
|
var searchController: UISearchController!
|
|
|
|
var searchControllerStatusOnAppearance: Bool? = nil
|
|
|
|
init(mastodonController: MastodonController) {
|
|
self.mastodonController = mastodonController
|
|
|
|
super.init(style: .insetGrouped)
|
|
|
|
dragEnabled = true
|
|
|
|
title = NSLocalizedString("Explore", comment: "explore tab title")
|
|
tabBarItem.image = UIImage(systemName: "magnifyingglass")
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: "basicCell")
|
|
|
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath)
|
|
|
|
switch item {
|
|
case .bookmarks:
|
|
cell.imageView!.image = UIImage(systemName: "bookmark.fill")
|
|
cell.textLabel!.text = NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
|
|
cell.accessoryType = .disclosureIndicator
|
|
|
|
case let .list(list):
|
|
cell.imageView!.image = UIImage(systemName: "list.bullet")
|
|
cell.textLabel!.text = list.title
|
|
cell.accessoryType = .disclosureIndicator
|
|
|
|
case .addList:
|
|
cell.imageView!.image = UIImage(systemName: "plus")
|
|
cell.textLabel!.text = NSLocalizedString("New List...", comment: "new list nav item title")
|
|
cell.accessoryType = .none
|
|
|
|
case let .savedHashtag(hashtag):
|
|
cell.imageView!.image = UIImage(systemName: "number")
|
|
cell.textLabel!.text = hashtag.name
|
|
cell.accessoryType = .disclosureIndicator
|
|
|
|
case .addSavedHashtag:
|
|
cell.imageView!.image = UIImage(systemName: "plus")
|
|
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
|
|
cell.accessoryType = .none
|
|
|
|
case let .savedInstance(url):
|
|
cell.imageView!.image = UIImage(systemName: "globe")
|
|
cell.textLabel!.text = url.host!
|
|
cell.accessoryType = .disclosureIndicator
|
|
|
|
case .findInstance:
|
|
cell.imageView!.image = UIImage(systemName: "magnifyingglass")
|
|
cell.textLabel!.text = NSLocalizedString("Find An Instance...", comment: "find instance nav item title")
|
|
cell.accessoryType = .none
|
|
}
|
|
|
|
return cell
|
|
})
|
|
dataSource.exploreController = self
|
|
|
|
let account = mastodonController.accountInfo!
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances])
|
|
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
|
snapshot.appendItems([.addList], toSection: .lists)
|
|
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
|
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
|
// the initial, static items should not be displayed with an animation
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
|
resultsController.exploreNavigationController = self.navigationController!
|
|
searchController = UISearchController(searchResultsController: resultsController)
|
|
searchController.searchResultsUpdater = resultsController
|
|
searchController.searchBar.autocapitalizationType = .none
|
|
searchController.searchBar.delegate = resultsController
|
|
definesPresentationContext = true
|
|
|
|
navigationItem.searchController = searchController
|
|
navigationItem.hidesSearchBarWhenScrolling = false
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
|
|
|
reloadLists()
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
func reloadLists() {
|
|
let request = Client.getLists()
|
|
mastodonController.run(request) { (response) in
|
|
guard case let .success(lists, _) = response else {
|
|
fatalError()
|
|
}
|
|
|
|
var snapshot = self.dataSource.snapshot()
|
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists))
|
|
snapshot.appendItems(lists.map { .list($0) } + [.addList], toSection: .lists)
|
|
|
|
DispatchQueue.main.async {
|
|
self.dataSource.apply(snapshot)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func savedHashtagsChanged() {
|
|
let account = mastodonController.accountInfo!
|
|
var snapshot = dataSource.snapshot()
|
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
|
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
|
dataSource.apply(snapshot)
|
|
}
|
|
|
|
@objc func savedInstancesChanged() {
|
|
let account = mastodonController.accountInfo!
|
|
var snapshot = dataSource.snapshot()
|
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
|
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
|
dataSource.apply(snapshot)
|
|
}
|
|
|
|
func deleteList(_ list: List) {
|
|
let title = String(format: NSLocalizedString("Are you sure want to delete the '%@' list?", comment: "delete list alert title"), list.title)
|
|
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: nil))
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
|
|
|
|
let request = List.delete(list)
|
|
self.mastodonController.run(request) { (response) in
|
|
guard case .success(_, _) = response else {
|
|
fatalError()
|
|
}
|
|
|
|
var snapshot = self.dataSource.snapshot()
|
|
snapshot.deleteItems([.list(list)])
|
|
DispatchQueue.main.async {
|
|
self.dataSource.apply(snapshot)
|
|
}
|
|
}
|
|
}))
|
|
present(alert, animated: true)
|
|
}
|
|
|
|
func removeSavedHashtag(_ hashtag: Hashtag) {
|
|
let account = mastodonController.accountInfo!
|
|
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
|
}
|
|
|
|
func removeSavedInstance(_ instanceURL: URL) {
|
|
let account = mastodonController.accountInfo!
|
|
SavedDataManager.shared.remove(instance: instanceURL, for: account)
|
|
}
|
|
|
|
// MARK: - Table view delegate
|
|
|
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
switch dataSource.itemIdentifier(for: indexPath) {
|
|
case nil:
|
|
return
|
|
|
|
case .bookmarks:
|
|
show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil)
|
|
|
|
case let .list(list):
|
|
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
|
|
|
case .addList:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
let alert = UIAlertController(title: NSLocalizedString("New List", comment: "new list alert title"), message: NSLocalizedString("Choose a title for your new list", comment: "new list alert message"), preferredStyle: .alert)
|
|
alert.addTextField(configurationHandler: nil)
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "new list alert cancel button"), style: .cancel, handler: nil))
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Create List", comment: "new list create button"), style: .default, handler: { (_) in
|
|
guard let title = alert.textFields?.first?.text else {
|
|
fatalError()
|
|
}
|
|
|
|
let request = Client.createList(title: title)
|
|
self.mastodonController.run(request) { (response) in
|
|
guard case let .success(list, _) = response else { fatalError() }
|
|
|
|
self.reloadLists()
|
|
|
|
DispatchQueue.main.async {
|
|
let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
|
listTimelineController.presentEditOnAppear = true
|
|
self.show(listTimelineController, sender: nil)
|
|
}
|
|
}
|
|
}))
|
|
present(alert, animated: true)
|
|
|
|
case let .savedHashtag(hashtag):
|
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
|
|
|
case .addSavedHashtag:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
|
|
present(navController, animated: true)
|
|
|
|
case let .savedInstance(url):
|
|
show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
|
|
|
|
case .findInstance:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
|
findController.instanceTimelineDelegate = self
|
|
let navController = UINavigationController(rootViewController: findController)
|
|
present(navController, animated: true)
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
|
return .delete
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
switch dataSource.itemIdentifier(for: indexPath) {
|
|
case .bookmarks:
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
|
return BookmarksTableViewController(mastodonController: self.mastodonController)
|
|
}, actionProvider: nil)
|
|
|
|
case let .list(list):
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
|
return ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
|
}, actionProvider: nil)
|
|
|
|
case let .savedHashtag(hashtag):
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
|
return HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
|
}, actionProvider: nil)
|
|
|
|
case let .savedInstance(url):
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
|
return InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController)
|
|
}, actionProvider: nil)
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension ExploreViewController {
|
|
enum Section: CaseIterable {
|
|
case bookmarks
|
|
case lists
|
|
case savedHashtags
|
|
case savedInstances
|
|
}
|
|
enum Item: Hashable {
|
|
case bookmarks
|
|
case list(List)
|
|
case addList
|
|
case savedHashtag(Hashtag)
|
|
case addSavedHashtag
|
|
case savedInstance(URL)
|
|
case findInstance
|
|
|
|
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case (.bookmarks, .bookmarks):
|
|
return true
|
|
case let (.list(a), .list(b)):
|
|
return a.id == b.id
|
|
case (.addList, .addList):
|
|
return true
|
|
case let (.savedHashtag(a), .savedHashtag(b)):
|
|
return a == b
|
|
case (.addSavedHashtag, .addSavedHashtag):
|
|
return true
|
|
case let (.savedInstance(a), .savedInstance(b)):
|
|
return a == b
|
|
case (.findInstance, .findInstance):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
func hash(into hasher: inout Hasher) {
|
|
switch self {
|
|
case .bookmarks:
|
|
hasher.combine("bookmarks")
|
|
case let .list(list):
|
|
hasher.combine("list")
|
|
hasher.combine(list.id)
|
|
case .addList:
|
|
hasher.combine("addList")
|
|
case let .savedHashtag(hashtag):
|
|
hasher.combine("savedHashtag")
|
|
hasher.combine(hashtag.name)
|
|
case .addSavedHashtag:
|
|
hasher.combine("addSavedHashtag")
|
|
case let .savedInstance(url):
|
|
hasher.combine("savedInstance")
|
|
hasher.combine(url)
|
|
case .findInstance:
|
|
hasher.combine("findInstance")
|
|
}
|
|
}
|
|
}
|
|
|
|
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
|
|
|
weak var exploreController: ExploreViewController?
|
|
|
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
switch section {
|
|
case 1:
|
|
return NSLocalizedString("Lists", comment: "explore lists section title")
|
|
case 2:
|
|
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
|
|
case 3:
|
|
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
switch itemIdentifier(for: indexPath) {
|
|
case .list(_):
|
|
return true
|
|
case .savedHashtag(_):
|
|
return true
|
|
case .savedInstance(_):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
|
guard editingStyle == .delete,
|
|
let exploreController = exploreController else {
|
|
return
|
|
}
|
|
|
|
switch itemIdentifier(for: indexPath) {
|
|
case let .list(list):
|
|
exploreController.deleteList(list)
|
|
case let .savedHashtag(hashtag):
|
|
exploreController.removeSavedHashtag(hashtag)
|
|
case let .savedInstance(url):
|
|
exploreController.removeSavedInstance(url)
|
|
default:
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
|
func didSaveInstance(url: URL) {
|
|
dismiss(animated: true) {
|
|
self.show(InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController), sender: nil)
|
|
}
|
|
}
|
|
|
|
func didUnsaveInstance(url: URL) {
|
|
dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
extension ExploreViewController {
|
|
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
|
let accountID = mastodonController.accountInfo?.id else {
|
|
return []
|
|
}
|
|
let provider: NSItemProvider
|
|
switch item {
|
|
case .bookmarks:
|
|
provider = NSItemProvider(object: UserActivityManager.bookmarksActivity())
|
|
case let .list(list):
|
|
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
|
|
provider = NSItemProvider(object: activity)
|
|
case let .savedHashtag(hashtag):
|
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
|
provider.registerObject(activity, visibility: .all)
|
|
}
|
|
case let .savedInstance(url):
|
|
provider = NSItemProvider(object: url as NSURL)
|
|
// todo: should dragging public timelines into new windows be supported?
|
|
case .addList:
|
|
return []
|
|
case .addSavedHashtag:
|
|
return []
|
|
case .findInstance:
|
|
return []
|
|
}
|
|
return [UIDragItem(itemProvider: provider)]
|
|
}
|
|
}
|