// // 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 { let mastodonController: MastodonController var dataSource: DataSource! var resultsController: SearchResultsViewController! var searchController: UISearchController! init(mastodonController: MastodonController) { self.mastodonController = mastodonController super.init(style: .insetGrouped) 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() 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 UIView.performWithoutAnimation { dataSource.apply(snapshot) } 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() } 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 } } 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 { 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) } }