// // StatusesTableViewController.swift // Tusker // // Created by Shadowfacts on 8/15/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class TimelineTableViewController: EnhancedTableViewController { var timeline: Timeline! weak var mastodonController: MastodonController! private var loaded = false var timelineSegments: [[(id: String, state: StatusState)]] = [] var newer: RequestRange? var older: RequestRange? init(for timeline: Timeline, mastodonController: MastodonController) { self.timeline = timeline self.mastodonController = mastodonController super.init(style: .plain) title = timeline.title tabBarItem.image = timeline.tabBarImage self.refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) 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() } 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() } } } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> 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) { // don't remove rows when jumping to the top, otherwise jumping back down might try to show removed rows // when scrolling upwards, decrement reference counts for old statuses, if necessary if !isCurrentlyScrollingToTop, scrollViewDirection < 0 { if indexPath.section <= timelineSegments.count - 2 { // decrement ref counts for all sections below the section below the current section // (e.g., there exist sections 0, 1, 2 and we're currently scrolling upwards in section 0, we want to remove section 2) // todo: this is in the hot path for scrolling, possibly move this to a background thread? let sectionsToRemove = indexPath.section + 1.. 2 * pageSize, indexPath.row < lastSection.count - (2 * pageSize) { // todo: this is in the hot path for scrolling, possibly move this to a background thread? let statusesToRemove = lastSection[lastSection.count - pageSize.. 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(_ sender: Any) { 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() } self.newer = pagination?.newer self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) if let newer = pagination?.newer { self.newer = newer } self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { DispatchQueue.main.async { let newIndexPaths = (0..