From 2e64500c350aa25a5d0cb1ffa001c07cd2314fd8 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 28 Jan 2023 15:03:13 -0500 Subject: [PATCH] Rewrite bookmarks VC using UICollectionView --- .../Pachyderm/Request/RequestRange.swift | 11 + Tusker.xcodeproj/project.pbxproj | 8 +- Tusker/Scenes/AuxiliarySceneDelegate.swift | 2 +- .../BookmarksTableViewController.swift | 196 ----------- .../Bookmarks/BookmarksViewController.swift | 310 ++++++++++++++++++ .../Explore/ExploreViewController.swift | 2 +- .../Main/MainSplitViewController.swift | 4 +- Tusker/Shortcuts/UserActivityManager.swift | 2 +- 8 files changed, 330 insertions(+), 205 deletions(-) delete mode 100644 Tusker/Screens/Bookmarks/BookmarksTableViewController.swift create mode 100644 Tusker/Screens/Bookmarks/BookmarksViewController.swift diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift index cf2e3cba..b683d194 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift @@ -15,6 +15,17 @@ public enum RequestRange { case before(id: String, count: Int?) /// Chronologically immediately after the given ID case after(id: String, count: Int?) + + public func withCount(_ count: Int) -> Self { + switch self { + case .default, .count(_): + return .count(count) + case .before(id: let id, count: _): + return .before(id: id, count: count) + case .after(id: let id, count: _): + return .after(id: id, count: count) + } + } } extension RequestRange { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 492b23ef..4c47eca3 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -105,7 +105,6 @@ D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; }; D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; }; D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; }; - D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; }; D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; }; D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; }; D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; }; @@ -327,6 +326,7 @@ D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; }; + D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; }; D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; }; @@ -509,7 +509,6 @@ D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = ""; }; D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.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 = ""; }; D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = ""; }; D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = ""; }; D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = ""; }; @@ -740,6 +739,7 @@ D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = ""; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = ""; }; D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = ""; }; + D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = ""; }; D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = ""; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = ""; }; D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; @@ -938,7 +938,7 @@ D627944823A6AD5100D38C68 /* Bookmarks */ = { isa = PBXGroup; children = ( - D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */, + D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */, ); path = Bookmarks; sourceTree = ""; @@ -2018,6 +2018,7 @@ D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */, + D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */, @@ -2069,7 +2070,6 @@ D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */, D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */, - D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */, diff --git a/Tusker/Scenes/AuxiliarySceneDelegate.swift b/Tusker/Scenes/AuxiliarySceneDelegate.swift index 46c9a7af..c4b56e13 100644 --- a/Tusker/Scenes/AuxiliarySceneDelegate.swift +++ b/Tusker/Scenes/AuxiliarySceneDelegate.swift @@ -85,7 +85,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate { return SearchViewController(mastodonController: mastodonController) case .bookmarks: - return BookmarksTableViewController(mastodonController: mastodonController) + return BookmarksViewController(mastodonController: mastodonController) case .myProfile: return MyProfileViewController(mastodonController: mastodonController) diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift deleted file mode 100644 index ade8ffcc..00000000 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// 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" - - let mastodonController: MastodonController - - private var loaded = false - - var statuses: [(id: String, state: CollapseState)] = [] - - var newer: RequestRange? - var older: RequestRange? - - init(mastodonController: MastodonController) { - self.mastodonController = mastodonController - - super.init(style: .plain) - - dragEnabled = true - - 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.allowsFocus = true - - tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) - - tableView.prefetchDataSource = self - - userActivity = UserActivityManager.bookmarksActivity() - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if !loaded { - loaded = true - - let request = Client.getBookmarks() - mastodonController.run(request) { (response) in - guard case let .success(statuses, pagination) = response else { fatalError() } - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) - self.newer = pagination?.newer - self.older = pagination?.older - - DispatchQueue.main.async { - self.tableView.reloadData() - } - } - } - } - } - - // 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 = Client.getBookmarks(range: older) - mastodonController.run(request) { (response) in - guard case let .success(newStatuses, pagination) = response else { fatalError() } - self.older = pagination?.older - self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { - let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map { - IndexPath(row: $0, section: 0) - } - self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) }) - - DispatchQueue.main.async { - UIView.performWithoutAnimation { - self.tableView.insertRows(at: newIndexPaths, with: .automatic) - } - } - } - } - } - - 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 = mastodonController.persistentContainer.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.id) - self.mastodonController.run(request) { (response) in - guard case let .success(newStatus, _) = response else { fatalError() } - self.mastodonController.persistentContainer.addOrUpdate(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 - } - - @objc private func handleStatusDeleted(_ notification: Foundation.Notification) { - guard let userInfo = notification.userInfo, - let accountID = mastodonController.accountInfo?.id, - userInfo["accountID"] as? String == accountID, - let statusIDs = userInfo["statusIDs"] as? [String] else { - return - } - let indicesToDelete = statusIDs - .compactMap { id in - self.statuses.firstIndex(where: { $0.id == id }) - } - self.statuses.remove(atOffsets: IndexSet(indicesToDelete)) - self.tableView.deleteRows(at: indicesToDelete.map { IndexPath(row: $0, section: 0) }, with: .automatic) - } - -} - -extension BookmarksTableViewController: TuskerNavigationDelegate { - var apiController: MastodonController! { mastodonController } -} - -extension BookmarksTableViewController: ToastableViewController { -} - -extension BookmarksTableViewController: MenuActionProvider { -} - -extension BookmarksTableViewController: StatusTableViewCellDelegate { - func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { - tableView.beginUpdates() - tableView.endUpdates() - } -} - -extension BookmarksTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - let ids = indexPaths.map { statuses[$0.row].id } - prefetchStatuses(with: ids) - } -} diff --git a/Tusker/Screens/Bookmarks/BookmarksViewController.swift b/Tusker/Screens/Bookmarks/BookmarksViewController.swift new file mode 100644 index 00000000..19eeaad6 --- /dev/null +++ b/Tusker/Screens/Bookmarks/BookmarksViewController.swift @@ -0,0 +1,310 @@ +// +// BookmarksViewController.swift +// Tusker +// +// Created by Shadowfacts on 12/15/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class BookmarksViewController: UIViewController, CollectionViewController { + + private static let pageSize = 40 + + let mastodonController: MastodonController + + var collectionView: UICollectionView! { + view as? UICollectionView + } + private var dataSource: UICollectionViewDiffableDataSource! + + private var state = State.unloaded + private var newer: RequestRange? + private var older: RequestRange? + + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + + super.init(nibName: nil, bundle: nil) + + title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.leadingSwipeActionsConfigurationProvider = { [unowned self] in + (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() + } + config.trailingSwipeActionsConfigurationProvider = { [unowned self] in + (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions() + } + config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in + guard let item = dataSource.itemIdentifier(for: indexPath) else { + return sectionConfig + } + var config = sectionConfig + if item.hideIndicators { + config.topSeparatorVisibility = .hidden + config.bottomSeparatorVisibility = .hidden + } else { + config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets + } + return config + } + let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in + let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment) + if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + section.contentInsetsReference = .readableContent + } + return section + } + view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dragDelegate = self + collectionView.allowsFocus = true + + dataSource = createDataSource() + } + + private func createDataSource() -> UICollectionViewDiffableDataSource { + let statusCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in + cell.delegate = self + cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil) + } + let loadingCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in + cell.indicator.startAnimating() + } + return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .status(id: let id, state: let state): + return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state)) + case .loadingIndicator: + return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ()) + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + clearSelectionOnAppear(animated: animated) + + if case .unloaded = state { + Task { + await loadInitial() + } + } + } + + private func apply(snapshot: NSDiffableDataSourceSnapshot, animatingDifferences: Bool) async { + await Task { @MainActor in + self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + }.value + } + + @MainActor + private func loadInitial() async { + state = .loadingInitial + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.bookmarks]) + snapshot.appendItems([.loadingIndicator]) + await apply(snapshot: snapshot, animatingDifferences: false) + + do { + let req = Client.getBookmarks(range: .count(BookmarksViewController.pageSize)) + let (statuses, pagination) = try await mastodonController.run(req) + newer = pagination?.newer + older = pagination?.older + + await mastodonController.persistentContainer.addAll(statuses: statuses) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.bookmarks]) + snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }) + await apply(snapshot: snapshot, animatingDifferences: true) + + state = .loaded + + } catch { + let config = ToastConfiguration(from: error, with: "Error Loading Bookmarks", in: self) { [weak self] toast in + toast.dismissToast(animated: true) + await self?.loadInitial() + } + showToast(configuration: config, animated: true) + + await apply(snapshot: NSDiffableDataSourceSnapshot(), animatingDifferences: false) + + state = .unloaded + } + } + + @MainActor + private func loadOlder() async { + guard case .loaded = state, + let older else { + return + } + state = .loadingOlder + + var snapshot = dataSource.snapshot() + snapshot.appendItems([.loadingIndicator]) + await apply(snapshot: snapshot, animatingDifferences: false) + + do { + let req = Client.getBookmarks(range: older.withCount(BookmarksViewController.pageSize)) + let (statuses, pagination) = try await mastodonController.run(req) + self.older = pagination?.older + + await mastodonController.persistentContainer.addAll(statuses: statuses) + + snapshot.deleteItems([.loadingIndicator]) + snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }) + await apply(snapshot: snapshot, animatingDifferences: true) + + state = .loaded + } catch { + let config = ToastConfiguration(from: error, with: "Error Loading Older Bookmarks", in: self) { [weak self] toast in + toast.dismissToast(animated: true) + await self?.loadOlder() + } + showToast(configuration: config, animated: true) + + snapshot.deleteItems([.loadingIndicator]) + await apply(snapshot: snapshot, animatingDifferences: false) + + state = .loaded + } + } + +} + +extension BookmarksViewController { + enum Section { + case bookmarks + } + enum Item: Equatable, Hashable { + case status(id: String, state: CollapseState) + case loadingIndicator + + var hideIndicators: Bool { + switch self { + case .loadingIndicator: + return true + default: + return false + } + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + switch (lhs, rhs) { + case (.status(id: let a, state: _), .status(id: let b, state: _)): + return a == b + case (.loadingIndicator, .loadingIndicator): + return true + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .status(id: let id, state: _): + hasher.combine(0) + hasher.combine(id) + case .loadingIndicator: + hasher.combine(1) + } + } + } +} + +extension BookmarksViewController { + enum State { + case unloaded + case loadingInitial + case loaded + case loadingOlder + } +} + +extension BookmarksViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + if indexPath.section == 0, + indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 { + Task { + await self.loadOlder() + } + } + } + + func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + if case .status(id: _, state: _) = dataSource.itemIdentifier(for: indexPath) { + return true + } else { + return false + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if case .status(id: let id, state: let state) = dataSource.itemIdentifier(for: indexPath) { + selected(status: id, state: state.copy()) + } + } + + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() + } + + func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) + } +} + +extension BookmarksViewController: UICollectionViewDragDelegate { + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? [] + } +} + +extension BookmarksViewController: TuskerNavigationDelegate { + var apiController: MastodonController! { mastodonController } +} + +extension BookmarksViewController: StatusCollectionViewCellDelegate { + 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) { + // bookmarks aren't filtered + } +} + +extension BookmarksViewController: TabBarScrollableViewController { + func tabBarScrollToTop() { + collectionView.scrollToTop() + } +} + +extension BookmarksViewController: StatusBarTappableViewController { + func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + collectionView.scrollToTop() + return .stop + } +} diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index e8e92f23..7e3925a1 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -334,7 +334,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect return case .bookmarks: - show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil) + show(BookmarksViewController(mastodonController: mastodonController), sender: nil) case .trendingStatuses: show(TrendingStatusesViewController(mastodonController: mastodonController), sender: nil) diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 95dcb590..30329717 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -301,7 +301,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate { toPrepend = searchVC } else { switch tabNavigationStack[1] { - case is BookmarksTableViewController: + case is BookmarksViewController: exploreItem = .bookmarks case let listVC as ListTimelineViewController: exploreItem = .list(listVC.list) @@ -374,7 +374,7 @@ fileprivate extension MainSidebarViewController.Item { case .explore: return SearchViewController(mastodonController: mastodonController) case .bookmarks: - return BookmarksTableViewController(mastodonController: mastodonController) + return BookmarksViewController(mastodonController: mastodonController) case .profileDirectory: return ProfileDirectoryViewController(mastodonController: mastodonController) case let .list(list): diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index f8dd75a3..5c4bd552 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -277,7 +277,7 @@ class UserActivityManager { mainViewController.select(tab: .explore) if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController { navigationController.popToRootViewController(animated: false) - navigationController.pushViewController(BookmarksTableViewController(mastodonController: mastodonController), animated: false) + navigationController.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: false) } }