// // StatusesTableViewController.swift // Tusker // // Created by Shadowfacts on 8/15/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class TimelineTableViewController: EnhancedTableViewController, StatusTableViewCellDelegate { var timeline: Timeline! weak var mastodonController: MastodonController! private var loaded = false var timelineSegments: [[(id: String, state: StatusState)]] = [] private let pageSize = 20 private var newer: RequestRange? private var older: RequestRange? private var lastLastVisibleRow: IndexPath? init(for timeline: Timeline, mastodonController: MastodonController) { self.timeline = timeline self.mastodonController = mastodonController super.init(style: .plain) title = timeline.title tabBarItem.image = timeline.tabBarImage #if !targetEnvironment(macCatalyst) self.refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshStatuses), for: .valueChanged) #endif addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title"))) userActivity = UserActivityManager.showTimelineActivity(timeline: timeline) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { guard let persistentContainer = mastodonController?.persistentContainer else { return } // decrement reference counts of any statuses we still have // if the app is currently being quit, this will not affect the persisted data because // the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:) for segment in timelineSegments { for (id, _) in segment { persistentContainer.status(for: id)?.decrementReferenceCount() } } } func statusID(for indexPath: IndexPath) -> String { return timelineSegments[indexPath.section][indexPath.row].id } override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 140 tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.prefetchDataSource = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loadInitialStatuses() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) pruneOffscreenRows() } func loadInitialStatuses() { guard !loaded else { return } loaded = true let request = Client.getStatuses(timeline: timeline) mastodonController.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } // todo: possible race condition here? we update the underlying data before waiting to reload the table view self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.newer = pagination?.newer self.older = pagination?.older self.mastodonController.persistentContainer.addAll(statuses: statuses) { DispatchQueue.main.async { self.tableView.reloadData() } } } } private func pruneOffscreenRows() { guard let lastVisibleRow = lastLastVisibleRow else { return } let lastSectionIndex = timelineSegments.count - 1 if lastVisibleRow.section < lastSectionIndex { // if there is a section below the last visible one let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex for section in sectionsToRemove { for (id, _) in timelineSegments.remove(at: section) { mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount() } } UIView.performWithoutAnimation { tableView.deleteSections(IndexSet(sectionsToRemove), with: .none) } } else if lastVisibleRow.section == lastSectionIndex { let lastSection = timelineSegments.last! let lastRowIndex = lastSection.count - 1 if lastVisibleRow.row < lastRowIndex - 20 { // if there are more than 20 rows in the current section below the last visible one let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + 20).. Int { return timelineSegments.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return timelineSegments[section].count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } let (id, state) = timelineSegments[indexPath.section][indexPath.row] cell.delegate = self cell.updateUI(statusID: id, state: state) return cell } // MARK: - Table view delegate override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // this assumes that indexPathsForVisibleRows is always in order lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last // load older statuses, if necessary if indexPath.section == timelineSegments.count - 1, indexPath.row == timelineSegments[indexPath.section].count - 1 { guard let older = older else { return } let request = Client.getStatuses(timeline: timeline, 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) { DispatchQueue.main.async { let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count) let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) } self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) UIView.performWithoutAnimation { self.tableView.insertRows(at: newIndexPaths, with: .none) } } } } } } 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? { return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() } // MARK: - Interaction @objc func refreshStatuses() { guard let newer = newer else { return } let request = Client.getStatuses(timeline: timeline, range: newer) mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } // If there is no new newer pagination, don't reset it, so that the user can continue refreshing for more recent statuses // Otherwise, when no new statuses were loaded, it would get reset and the the user would be unable to refresh if let newer = pagination?.newer { self.newer = newer } self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { DispatchQueue.main.async { self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) let newIndexPaths = (0..