From 036791bd39425d0b502e7230a5faf7753f73e300 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 17 Dec 2019 00:22:25 -0500 Subject: [PATCH] Replace Search tab with Explore tab - Search controller (functionally the same, presents results on top of explore menu) - Add bookmarks screen See #63 --- Pachyderm/Client.swift | 8 + Tusker.xcodeproj/project.pbxproj | 36 +++- Tusker/AppDelegate.swift | 20 +- .../BookmarksTableViewController.swift | 172 ++++++++++++++++++ .../Explore/ExploreViewController.swift | 111 +++++++++++ .../Main/MainTabBarViewController.swift | 4 +- ...wift => SearchResultsViewController.swift} | 49 ++--- Tusker/Shortcuts/UserActivityManager.swift | 28 ++- Tusker/Shortcuts/UserActivityType.swift | 3 + Tusker/Views/BasicTableViewCell.xib | 30 +++ Tusker/XCallbackURL/XCBActions.swift | 15 +- 11 files changed, 428 insertions(+), 48 deletions(-) create mode 100644 Tusker/Screens/Bookmarks/BookmarksTableViewController.swift create mode 100644 Tusker/Screens/Explore/ExploreViewController.swift rename Tusker/Screens/Search/{SearchTableViewController.swift => SearchResultsViewController.swift} (85%) create mode 100644 Tusker/Views/BasicTableViewCell.xib diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 895a0c37..cd35a45a 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -308,6 +308,14 @@ public class Client { return timeline.request(range: range) } + + // MARK: Bookmarks + public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { + var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks") + request.range = range + return request + } + } extension Client { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 6f2ebe98..943e11f7 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -81,6 +81,9 @@ D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */; }; D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */; }; D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; }; + D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943D23A564D400D38C68 /* ExploreViewController.swift */; }; + D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; }; + D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; }; D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; }; D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; }; D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; }; @@ -184,7 +187,7 @@ D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; }; D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */; }; D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; - D6BC9DDA232D8BE5002CA326 /* SearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */; }; + D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; @@ -349,6 +352,9 @@ D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStatusActivity.swift; sourceTree = ""; }; D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnbookmarkStatusActivity.swift; sourceTree = ""; }; D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = ""; }; + D627943D23A564D400D38C68 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = ""; }; + D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = ""; }; + D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = ""; }; D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = ""; }; D627FF78217E950100CC0648 /* DraftsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftsTableViewController.xib; sourceTree = ""; }; D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = ""; }; @@ -449,7 +455,7 @@ D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = ""; }; D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsMode.swift; sourceTree = ""; }; D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = ""; }; - D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTableViewController.swift; sourceTree = ""; }; + D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; @@ -717,6 +723,22 @@ path = "Status Activities"; sourceTree = ""; }; + D627943C23A5635D00D38C68 /* Explore */ = { + isa = PBXGroup; + children = ( + D627943D23A564D400D38C68 /* ExploreViewController.swift */, + ); + path = Explore; + sourceTree = ""; + }; + D627944823A6AD5100D38C68 /* Bookmarks */ = { + isa = PBXGroup; + children = ( + D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */, + ); + path = Bookmarks; + sourceTree = ""; + }; D627FF77217E94F200CC0648 /* Drafts */ = { isa = PBXGroup; children = ( @@ -750,12 +772,14 @@ D641C785213DD83B004B4513 /* Conversation */, D641C786213DD852004B4513 /* Notifications */, D641C787213DD862004B4513 /* Compose */, + D627943C23A5635D00D38C68 /* Explore */, D6BC9DD8232D8BCA002CA326 /* Search */, D641C788213DD86D004B4513 /* Large Image */, 0411610422B4571E0030A9B7 /* Attachment */, 0411610522B457290030A9B7 /* Gallery */, D6A3BC822321F69400FD64D5 /* Account List */, D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */, + D627944823A6AD5100D38C68 /* Bookmarks */, D641C789213DD87E004B4513 /* Preferences */, ); path = Screens; @@ -1082,7 +1106,7 @@ D6BC9DD8232D8BCA002CA326 /* Search */ = { isa = PBXGroup; children = ( - D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */, + D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */, ); path = Search; sourceTree = ""; @@ -1097,6 +1121,7 @@ 04ED00B021481ED800567C53 /* SteppedProgressView.swift */, D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */, D627943123A5466600D38C68 /* SelectableTableViewCell.swift */, + D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */, D67C57A721E2649B00C3118B /* Account Detail */, D67C57B021E28F9400C3118B /* Compose Status Reply */, D60C07E221E817560057FAA8 /* Compose Media */, @@ -1481,6 +1506,7 @@ D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, + D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */, 0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, @@ -1622,7 +1648,7 @@ D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, - D6BC9DDA232D8BE5002CA326 /* SearchTableViewController.swift in Sources */, + D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, @@ -1630,6 +1656,7 @@ D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */, D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, + D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, @@ -1640,6 +1667,7 @@ D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */, + D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */, D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 34432616..ffadcea0 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -40,19 +40,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return XCBManager.handle(url: url) } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false), let tabBarController = window!.rootViewController as? MainTabBarViewController, - let navigationController = tabBarController.viewControllers?[3] as? UINavigationController, - let searchController = navigationController.viewControllers.first as? SearchTableViewController { + let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController, + let exploreController = exploreNavController.viewControllers.first as? ExploreViewController { + + tabBarController.select(tab: .explore) + exploreNavController.popToRootViewController(animated: false) + + exploreController.loadViewIfNeeded() + exploreController.searchController.isActive = true components.scheme = "https" - - tabBarController.selectedIndex = 3 - navigationController.popToRootViewController(animated: false) - - searchController.loadViewIfNeeded() - let query = components.url!.absoluteString - searchController.searchController.searchBar.text = query - searchController.performSearch(query: query) + exploreController.searchController.searchBar.text = query + exploreController.resultsController.performSearch(query: query) return true } diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift new file mode 100644 index 00000000..ec478fb9 --- /dev/null +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -0,0 +1,172 @@ +// +// BookmarksTableViewController.swift +// Tusker +// +// Created by Shadowfacts on 12/15/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class BookmarksTableViewController: EnhancedTableViewController { + + private let statusCell = "statusCell" + + var statuses: [(id: String, state: StatusState)] = [] { + didSet { + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + } + + var newer: RequestRange? + var older: RequestRange? + + init() { + super.init(style: .plain) + + title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 140 + + tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) + + tableView.prefetchDataSource = self + + let request = MastodonController.client.getBookmarks() + MastodonController.client.run(request) { (response) in + guard case let .success(statuses, pagination) = response else { fatalError() } + MastodonCache.addAll(statuses: statuses) + self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) + self.newer = pagination?.newer + self.older = pagination?.older + } + + userActivity = UserActivityManager.bookmarksActivity() + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return statuses.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell + cell.delegate = self + let (id, state) = statuses[indexPath.row] + cell.updateUI(statusID: id, state: state) + return cell + } + + // MARK: - Table view delegate + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard indexPath.row == statuses.count, let older = older else { + return + } + + let request = MastodonController.client.getBookmarks(range: older) + MastodonController.client.run(request) { (response) in + guard case let .success(newStatuses, pagination) = response else { fatalError() } + self.older = pagination?.older + MastodonCache.addAll(statuses: newStatuses) + self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) }) + } + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + + guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { + return cellConfig + } + + let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in + let request = Status.unbookmark(status) + MastodonController.client.run(request) { (response) in + guard case let .success(newStatus, _) = response else { fatalError() } + MastodonCache.add(status: newStatus) + self.statuses.remove(at: indexPath.row) + } + } + unbookmarkAction.image = UIImage(systemName: "bookmark.fill") + + let config: UISwipeActionsConfiguration + if let cellConfig = cellConfig { + config = UISwipeActionsConfiguration(actions: cellConfig.actions + [unbookmarkAction]) + config.performsFirstActionWithFullSwipe = cellConfig.performsFirstActionWithFullSwipe + } else { + config = UISwipeActionsConfiguration(actions: [unbookmarkAction]) + config.performsFirstActionWithFullSwipe = false + } + return config + } + + override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] { + guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { return [] } + return [ + UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in + let request = Status.unbookmark(status) + MastodonController.client.run(request) { (response) in + guard case let .success(newStatus, _) = response else { fatalError() } + MastodonCache.add(status: newStatus) + self.statuses.remove(at: indexPath.row) + } + }) + ] + } + +} + +extension BookmarksTableViewController: StatusTableViewCellDelegate { + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { + tableView.beginUpdates() + tableView.endUpdates() + } +} + +extension BookmarksTableViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + for indexPath in indexPaths { + guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + ImageCache.avatars.get(status.account.avatar, completion: nil) + for attachment in status.attachments where attachment.kind == .image { + ImageCache.attachments.get(attachment.url, completion: nil) + } + } + } + + func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { + for indexPath in indexPaths { + guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + ImageCache.avatars.cancel(status.account.avatar) + for attachment in status.attachments where attachment.kind == .image { + ImageCache.attachments.cancel(attachment.url) + } + } + } +} diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift new file mode 100644 index 00000000..72a20262 --- /dev/null +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -0,0 +1,111 @@ +// +// ExploreViewController.swift +// Tusker +// +// Created by Shadowfacts on 12/14/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Combine + +class ExploreViewController: EnhancedTableViewController { + + var dataSource: UITableViewDiffableDataSource! + + var resultsController: SearchResultsViewController! + var searchController: UISearchController! + + let searchSubject = PassthroughSubject() + + init() { + 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 + switch item { + case .bookmarks: + let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath) + cell.imageView!.image = UIImage(systemName: "bookmark.fill") + cell.textLabel!.text = NSLocalizedString("Bookmarks", comment: "bookmarks nav item title") + cell.accessoryType = .disclosureIndicator + return cell + + case let .list(id): + let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath) + cell.imageView!.image = nil + cell.textLabel!.text = id + cell.accessoryType = .disclosureIndicator + return cell + } + }) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.bookmarks, .lists]) + snapshot.appendItems([.bookmarks], toSection: .bookmarks) + // the initial, static items should not be displayed with an animation + UIView.performWithoutAnimation { + dataSource.apply(snapshot) + } + + resultsController = SearchResultsViewController() + 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 + } + + // MARK: - Table view delegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch dataSource.itemIdentifier(for: indexPath) { + case nil: + return + + case .bookmarks: + show(BookmarksTableViewController(), sender: nil) + + case let .list(id): + show(TimelineTableViewController(for: .list(id: id)), sender: nil) + } + } + +} + +extension ExploreViewController { + enum Section: CaseIterable { + case bookmarks + case lists + } + enum Item: Hashable { + case bookmarks + case list(id: String) + } + class DataSource: UITableViewDiffableDataSource { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 1: + return NSLocalizedString("Lists", comment: "explore lists section title") + default: + return nil + } + } + } +} diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 47fa35cb..02590765 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -19,7 +19,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { embedInNavigationController(TimelinesPageViewController()), embedInNavigationController(NotificationsPageViewController()), ComposeViewController(), - embedInNavigationController(SearchTableViewController()), + embedInNavigationController(ExploreViewController()), embedInNavigationController(MyProfileTableViewController()), ] } @@ -53,7 +53,7 @@ extension MainTabBarViewController { case timelines case notifications case compose - case search + case explore case myProfile } diff --git a/Tusker/Screens/Search/SearchTableViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift similarity index 85% rename from Tusker/Screens/Search/SearchTableViewController.swift rename to Tusker/Screens/Search/SearchResultsViewController.swift index 42820059..30752c0d 100644 --- a/Tusker/Screens/Search/SearchTableViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -1,5 +1,5 @@ // -// SearchTableViewController.swift +// SearchResultsViewController.swift // Tusker // // Created by Shadowfacts on 9/14/19. @@ -14,11 +14,12 @@ fileprivate let accountCell = "accountCell" fileprivate let statusCell = "statusCell" fileprivate let hashtagCell = "hashtagCell" -class SearchTableViewController: EnhancedTableViewController { +class SearchResultsViewController: EnhancedTableViewController { - var dataSource: UITableViewDiffableDataSource! - let searchController = UISearchController(searchResultsController: nil) + weak var exploreNavigationController: UINavigationController? + var dataSource: UITableViewDiffableDataSource! + var activityIndicator: UIActivityIndicatorView! let searchSubject = PassthroughSubject() @@ -27,8 +28,7 @@ class SearchTableViewController: EnhancedTableViewController { init() { super.init(style: .grouped) - title = NSLocalizedString("Search", comment: "search tab title") - tabBarItem.image = UIImage(systemName: "magnifyingglass") + title = NSLocalizedString("Search", comment: "search screen title") } required init?(coder: NSCoder) { @@ -62,13 +62,6 @@ class SearchTableViewController: EnhancedTableViewController { } }) - searchController.searchResultsUpdater = self - searchController.obscuresBackgroundDuringPresentation = false - searchController.searchBar.placeholder = NSLocalizedString("Search or Enter URL", comment: "search field placeholder") - searchController.searchBar.delegate = self - navigationItem.searchController = searchController - definesPresentationContext = true - activityIndicator = UIActivityIndicatorView(style: .large) activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.isHidden = true @@ -83,33 +76,43 @@ class SearchTableViewController: EnhancedTableViewController { .map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0 != self.currentQuery } .sink(receiveValue: performSearch(query:)) - + userActivity = UserActivityManager.searchActivity() } + override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? { + // if we're showing a view controller, we need to go up to the explore VC's nav controller + // the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent + if action == #selector(UIViewController.show(_:sender:)), + let exploreNavController = exploreNavigationController { + return exploreNavController + } + return super.targetViewController(forAction: action, sender: sender) + } + func performSearch(query: String?) { guard let query = query, !query.isEmpty else { self.dataSource.apply(NSDiffableDataSourceSnapshot()) return } self.currentQuery = query - + if self.dataSource.snapshot().numberOfItems == 0 { activityIndicator.isHidden = false activityIndicator.startAnimating() } - + let request = MastodonController.client.search(query: query, resolve: true, limit: 10) MastodonController.client.run(request) { (response) in guard case let .success(results, _) = response else { fatalError() } - + DispatchQueue.main.async { self.activityIndicator.isHidden = true self.activityIndicator.stopAnimating() } - + guard self.currentQuery == query else { return } - + var snapshot = NSDiffableDataSourceSnapshot() if !results.accounts.isEmpty { snapshot.appendSections([.accounts]) @@ -132,7 +135,7 @@ class SearchTableViewController: EnhancedTableViewController { } -extension SearchTableViewController { +extension SearchResultsViewController { enum Section: CaseIterable { case accounts case hashtags @@ -166,20 +169,20 @@ extension SearchTableViewController { } } -extension SearchTableViewController: UISearchResultsUpdating { +extension SearchResultsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { searchSubject.send(searchController.searchBar.text) } } -extension SearchTableViewController: UISearchBarDelegate { +extension SearchResultsViewController: UISearchBarDelegate { func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { // perform a search immedaitely when the search button is pressed performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines)) } } -extension SearchTableViewController: StatusTableViewCellDelegate { +extension SearchResultsViewController: StatusTableViewCellDelegate { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { tableView.beginUpdates() tableView.endUpdates() diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index f1ee542f..4b9c09dc 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -145,8 +145,8 @@ class UserActivityManager { } } - // MARK: - Search - + // MARK: - Explore + static func searchActivity() -> NSUserActivity { let activity = NSUserActivity(type: .search) activity.isEligibleForPrediction = true @@ -157,9 +157,29 @@ class UserActivityManager { static func handleSearch(activity: NSUserActivity) { let tabBarController = getMainTabBarController() - tabBarController.select(tab: .search) - if let navigationController = tabBarController.getTabController(tab: .search) as? UINavigationController { + tabBarController.select(tab: .explore) + if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController, + let exploreController = navigationController.viewControllers.first as? ExploreViewController { navigationController.popToRootViewController(animated: false) + exploreController.searchController.isActive = true + exploreController.searchController.searchBar.becomeFirstResponder() + } + } + + static func bookmarksActivity() -> NSUserActivity { + let activity = NSUserActivity(type: .bookmarks) + activity.isEligibleForPrediction = true + activity.title = NSLocalizedString("View Bookmarks", comment: "bookmarks shortcut title") + activity.suggestedInvocationPhrase = NSLocalizedString("Show my bookmarks in Tusker", comment: "bookmarks shortcut invocation phrase") + return activity + } + + static func handleBookmarks(activity: NSUserActivity) { + let tabBarController = getMainTabBarController() + tabBarController.select(tab: .explore) + if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController { + navigationController.popToRootViewController(animated: false) + navigationController.pushViewController(BookmarksTableViewController(), animated: false) } } diff --git a/Tusker/Shortcuts/UserActivityType.swift b/Tusker/Shortcuts/UserActivityType.swift index 86b7a2d2..b525fbc5 100644 --- a/Tusker/Shortcuts/UserActivityType.swift +++ b/Tusker/Shortcuts/UserActivityType.swift @@ -14,6 +14,7 @@ enum UserActivityType: String { case checkMentions = "net.shadowfacts.Tusker.activity.check-mentions" case showTimeline = "net.shadowfacts.Tusker.activity.show-timeline" case search = "net.shadowfacts.Tusker.activity.search" + case bookmarks = "net.shadowfacts.Tusker.activity.bookmarks" } extension UserActivityType { @@ -29,6 +30,8 @@ extension UserActivityType { return UserActivityManager.handleShowTimeline case .search: return UserActivityManager.handleSearch + case .bookmarks: + return UserActivityManager.handleBookmarks } } } diff --git a/Tusker/Views/BasicTableViewCell.xib b/Tusker/Views/BasicTableViewCell.xib new file mode 100644 index 00000000..2698260c --- /dev/null +++ b/Tusker/Views/BasicTableViewCell.xib @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 4494f288..15b42eab 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -329,12 +329,17 @@ struct XCBActions { let query = request.arguments["query"]! let tabBarController = getMainTabBarController() - if let navigationController = tabBarController.getTabController(tab: .search) as? UINavigationController, - let searchController = navigationController.viewControllers.first as? SearchTableViewController { - tabBarController.select(tab: .search) + if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController, + let exploreController = navigationController.viewControllers.first as? ExploreViewController { + + tabBarController.select(tab: .explore) navigationController.popToRootViewController(animated: false) - searchController.searchController.searchBar.text = query - searchController.performSearch(query: query) + + exploreController.loadViewIfNeeded() + exploreController.searchController.isActive = true + + exploreController.searchController.searchBar.text = query + exploreController.resultsController.performSearch(query: query) } else { session.complete(with: .error) }