diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 2934acd8..e8d48df7 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class ProfileStatusesViewController: TimelineLikeTableViewController { +class ProfileStatusesViewController: DiffableTimelineLikeTableViewController { weak var mastodonController: MastodonController! @@ -43,50 +43,50 @@ class ProfileStatusesViewController: TimelineLikeTableViewController String { return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title") } - override func headerSectionsCount() -> Int { - return 1 + override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? { + let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell + + cell.delegate = self + // todo: dataSource.sectionIdentifier is only available on iOS 15 + cell.showPinned = dataSource.snapshot().indexOfSection(.pinned) == indexPath.section + cell.updateUI(statusID: item.id, state: item.state) + + return cell } - override func loadInitial() { + override func loadInitialItems(completion: @escaping (LoadResult) -> Void) { guard accountID != nil else { + completion(.failure(.noClient)) return } - if !loaded { - loadPinnedStatuses() - } - - super.loadInitial() - } - - private func loadPinnedStatuses() { - guard kind == .statuses else { - return - } - getPinnedStatuses { (response) in - guard case let .success(statuses, _) = response, - !statuses.isEmpty else { - // todo: error message - return - } - - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - let items = statuses.map { ($0.id, StatusState.unknown) } - DispatchQueue.main.async { - UIView.performWithoutAnimation { - if self.sections.count < 1 { - self.sections.append(items) - self.tableView.insertSections(IndexSet(integer: 0), with: .none) + getStatuses { (response) in + switch response { + case let .failure(error): + completion(.failure(.client(error))) + + case let .success(statuses, pagination): + self.older = pagination?.older + self.newer = pagination?.newer + + self.mastodonController.persistentContainer.addAll(statuses: statuses) { + DispatchQueue.main.async { + var snapshot = self.dataSource.snapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses) + if self.kind == .statuses { + self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion) } else { - self.sections[0] = items - self.tableView.reloadSections(IndexSet(integer: 0), with: .none) + completion(.success(snapshot)) } } } @@ -94,64 +94,94 @@ class ProfileStatusesViewController: TimelineLikeTableViewController Void) { - getStatuses { (response) in - guard case let .success(statuses, pagination) = response, - !statuses.isEmpty else { - // todo: error message - completion([]) - return - } - - self.older = pagination?.older - self.newer = pagination?.newer - - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - completion(statuses.map { ($0.id, .unknown) }) + private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { + guard kind == .statuses else { + completion(.success(snapshot())) + return + } + getPinnedStatuses { (response) in + switch response { + case let .failure(error): + completion(.failure(.client(error))) + + case let .success(statuses, _): + self.mastodonController.persistentContainer.addAll(statuses: statuses) { + DispatchQueue.main.async { + var snapshot = snapshot() + if snapshot.indexOfSection(.pinned) != nil { + snapshot.deleteSections([.pinned]) + } + snapshot.insertSections([.pinned], beforeSection: .statuses) + snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .pinned) + completion(.success(snapshot)) + } + } } } } - override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) { + override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { guard let older = older else { - completion([]) + completion(.failure(.noOlder)) return } getStatuses(for: older) { (response) in - guard case let .success(statuses, pagination) = response else { - // todo: error message - completion([]) - return - } - - self.older = pagination?.older - - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - completion(statuses.map { ($0.id, .unknown) }) + switch response { + case let .failure(error): + completion(.failure(.client(error))) + + case let .success(statuses, pagination): + guard !statuses.isEmpty else { + completion(.failure(.noOlder)) + return + } + + if let older = pagination?.older { + self.older = older + } + + self.mastodonController.persistentContainer.addAll(statuses: statuses) { + var snapshot = currentSnapshot() + snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses) + completion(.success(snapshot)) + } } } } + - override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) { + override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { guard let newer = newer else { - completion([]) + completion(.failure(.noNewer)) return } - + getStatuses(for: newer) { (response) in - guard case let .success(statuses, pagination) = response else { - // todo: error message - completion([]) - return - } - - if let newer = pagination?.newer { - self.newer = newer - } - - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - completion(statuses.map { ($0.id, .unknown) }) + switch response { + case let .failure(error): + completion(.failure(.client(error))) + + case let .success(statuses, pagination): + guard !statuses.isEmpty else { + completion(.failure(.noNewer)) + return + } + + if let newer = pagination?.newer { + self.newer = newer + } + + self.mastodonController.persistentContainer.addAll(statuses: statuses) { + var snapshot = currentSnapshot() + let items = statuses.map { Item(id: $0.id, state: .unknown) } + if let first = snapshot.itemIdentifiers(inSection: .statuses).first { + snapshot.insertItems(items, beforeItem: first) + } else { + snapshot.appendItems(items, toSection: .statuses) + } + completion(.success(snapshot)) + } } } } @@ -178,53 +208,20 @@ class ProfileStatusesViewController: TimelineLikeTableViewController TimelineEntry in - let state: StatusState - if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) { - state = oldState - } else { - state = .unknown - } - return (status.id, state) - } + loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in + switch result { + case .failure(_): + break + + case let .success(snapshot): DispatchQueue.main.async { - UIView.performWithoutAnimation { - if self.sections.count < 1 { - self.sections.append(pinnedStatues) - self.tableView.insertSections(IndexSet(integer: 0), with: .none) - } else { - self.sections[0] = pinnedStatues - self.tableView.reloadSections(IndexSet(integer: 0), with: .none) - } - } + self.dataSource.apply(snapshot) } } } } } - // MARK: - UITableViewDatasource - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell - - cell.delegate = self - cell.showPinned = indexPath.section == 0 - - let (id, state) = item(for: indexPath) - cell.updateUI(statusID: id, state: state) - - return cell - } - } extension ProfileStatusesViewController { @@ -233,6 +230,17 @@ extension ProfileStatusesViewController { } } +extension ProfileStatusesViewController { + enum Section: CaseIterable { + case pinned + case statuses + } + struct Item: Hashable { + let id: String + let state: StatusState + } +} + extension ProfileStatusesViewController: TuskerNavigationDelegate { var apiController: MastodonController { mastodonController } } @@ -245,18 +253,12 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate { extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - let ids = indexPaths.map { item(for: $0).id } + let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id } prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - let ids: [String] = indexPaths.compactMap { - guard $0.section < sections.count, - $0.row < sections[$0.section].count else { - return nil - } - return item(for: $0).id - } + let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id } cancelPrefetchingStatuses(with: ids) } } diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index 49df4883..11ecd62e 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -211,9 +211,8 @@ class ProfileViewController: UIPageViewController { // Layout and update the table view, otherwise the content jumps around when first scrolling it, // if old was not scrolled all the way to the top new.tableView.layoutIfNeeded() - UIView.performWithoutAnimation { - new.tableView.performBatchUpdates(nil, completion: nil) - } + let snapshot = new.dataSource.snapshot() + new.dataSource.apply(snapshot, animatingDifferences: false) completion?(finished) } diff --git a/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift b/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift index ca53cc2e..d73ae518 100644 --- a/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift +++ b/Tusker/Screens/Utilities/DiffableTimelineLikeTableViewController.swift @@ -88,7 +88,10 @@ class DiffableTimelineLikeTableViewController