// // ProfileStatusesViewController.swift // Tusker // // Created by Shadowfacts on 7/3/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class ProfileStatusesViewController: DiffableTimelineLikeTableViewController { weak var mastodonController: MastodonController! private(set) var headerView: ProfileHeaderView! var accountID: String! let kind: Kind private var older: RequestRange? private var newer: RequestRange? init(accountID: String?, kind: Kind, mastodonController: MastodonController) { self.accountID = accountID self.kind = kind self.mastodonController = mastodonController super.init() dragEnabled = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") } func updateUI(account: AccountMO) { if isViewLoaded { reloadInitial() } } override class func refreshCommandTitle() -> String { return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title") } 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 loadInitialItems(completion: @escaping (LoadResult) -> Void) { guard accountID != nil else { completion(.failure(.noClient)) return } 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 { completion(.success(snapshot)) } } } } } } 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 loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { guard let older = older else { completion(.failure(.noOlder)) return } getStatuses(for: older) { (response) in 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 loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) { guard let newer = newer else { completion(.failure(.noNewer)) return } getStatuses(for: newer) { (response) in 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)) } } } } private func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) { let request: Request<[Status]> switch kind { case .statuses: request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true) case .withReplies: request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: false) case .onlyMedia: request = Account.getStatuses(accountID, range: range, onlyMedia: true, pinned: false, excludeReplies: false) } mastodonController.run(request, completion: completion) } private func getPinnedStatuses(completion: @escaping Client.Callback<[Status]>) { let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false) mastodonController.run(request, completion: completion) } override func refresh() { super.refresh() if kind == .statuses { loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in switch result { case .failure(_): break case let .success(snapshot): DispatchQueue.main.async { self.dataSource.apply(snapshot) } } } } } } extension ProfileStatusesViewController { enum Kind { case statuses, withReplies, onlyMedia } } 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 } } extension ProfileStatusesViewController: StatusTableViewCellDelegate { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { cellHeightChanged() } } extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id } prefetchStatuses(with: ids) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id } cancelPrefetchingStatuses(with: ids) } }