[ABANDONED] Mastodon iOS client.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

360 lines
13 KiB

//
// TimelineTableViewController.swift
// elpha-ios
//
// Created by Dwayne Harris on 10/1/18.
// Copyright © 2018 Elpha. All rights reserved.
//
import AlamofireImage
import CoreData
import MastodonKit
import UIKit
class TimelineTableViewController: UITableViewController, TimelinesTableViewControllerDelegate {
var fetchedResultsController: NSFetchedResultsController<StatusMO>? = nil
let fetchLimit = 50
var loading: Bool = false {
didSet {
DispatchQueue.main.async {
if self.loading {
self.refreshControl?.beginRefreshing()
} else {
self.refreshControl?.endRefreshing()
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
initializeFetchedResultsController()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 500
let moreButtonItem = UIBarButtonItem(image: UIImage(named: "More"), style: .plain, target: self, action: #selector(more))
let cleanButtonItem = UIBarButtonItem(image: UIImage(named: "Refresh CCW"), style: .plain, target: self, action: #selector(clean))
let composeButtonItem = UIBarButtonItem(image: UIImage(named: "Compose"), style: .plain, target: self, action: #selector(compose))
navigationItem.rightBarButtonItems = [composeButtonItem, cleanButtonItem, moreButtonItem]
if let timeline = AuthenticationManager.shared.session?.timeline {
navigationItem.title = timeline.name
} else {
navigationItem.title = "Home"
}
refreshControl?.addTarget(self, action: #selector(self.fetchTimelineWithDefaultRange), for: .valueChanged)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fetchTimelineWithDefaultRange()
}
@objc func clean() {
guard let timeline = AuthenticationManager.shared.session?.timeline else {
return
}
timeline.mutableSetValue(forKey: "statuses").removeAllObjects()
if let boundaries = timeline.boundaries?.allObjects as? [TimelineBoundaryMO] {
boundaries.forEach { CoreDataManager.shared.context.delete($0) }
}
timeline.mutableSetValue(forKey: "boundaries").removeAllObjects()
CoreDataManager.shared.saveContext()
}
@objc func more() {
performSegue(withIdentifier: "TimelinesSegue", sender: self)
}
@objc func compose() {
let alertController = UIAlertController(title: "Compose", message: "Toot", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alertController, animated: true)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "TimelinesSegue" {
if let destination = segue.destination as? TimelinesTableViewController {
destination.delegate = self
}
}
}
func didSelect(timeline: TimelineMO) {
navigationItem.title = timeline.name
AuthenticationManager.shared.session?.timeline = timeline
CoreDataManager.shared.saveContext()
initializeFetchedResultsController()
tableView.reloadData()
}
func createDefaultTimelines(account: AccountMO) {
let names = [
"Home",
"Local",
"Federated",
"Favorites",
]
for (index, name) in names.enumerated() {
let timeline = TimelineMO(context: CoreDataManager.shared.context)
timeline.name = name
timeline.account = account
timeline.order = Int16(index)
}
CoreDataManager.shared.saveContext()
}
func fetchStatuses(request: Request<[Status]>, forTimeline timeline: TimelineMO, completion: @escaping ([UpsertResult<StatusMO>], Error?) -> Void) {
guard let client = AuthenticationManager.shared.mkClient else {
completion([], nil)
return
}
print("Request \(request)")
client.run(request) { result in
switch result {
case .success(let remoteStatuses, _):
DispatchQueue.main.async {
let statuses = remoteStatuses.compactMap { status in
return MastodonDataManager.upsertStatus(status)
}
for (index, statusResult) in statuses.enumerated() {
if index == 0 {
let boundary = TimelineBoundaryMO(context: CoreDataManager.shared.context)
boundary.status = statusResult.model
boundary.timeline = timeline
boundary.start = true
boundary.fetchedAt = Date()
} else if index == statuses.count - 1 {
let boundary = TimelineBoundaryMO(context: CoreDataManager.shared.context)
boundary.status = statusResult.model
boundary.timeline = timeline
boundary.start = false
boundary.fetchedAt = Date()
} else {
let request = NSFetchRequest<TimelineBoundaryMO>(entityName: "TimelineBoundary")
request.predicate = NSPredicate(format: "status == %@ AND timeline == %@", statusResult.model, timeline)
do {
let results = try CoreDataManager.shared.context.fetch(request)
results.forEach { CoreDataManager.shared.context.delete($0) }
} catch {
print("\(error)")
}
}
statusResult.model.addToTimelines(timeline)
}
CoreDataManager.shared.saveContext()
self.loading = false
completion(statuses, nil)
}
case .failure(let error):
completion([], error)
}
}
}
@objc func fetchTimelineWithDefaultRange() {
fetchTimeline(withRange: .limit(fetchLimit)) { error in
guard error == nil else {
return
}
}
}
func fetchTimeline(withRange requestRange: RequestRange, completion: @escaping (Error?) -> Void) {
guard let session = AuthenticationManager.shared.session, let account = session.account else {
completion(nil)
return
}
if session.timeline == nil {
if let timelines = account.timelines, timelines.count == 0 {
createDefaultTimelines(account: account)
}
let request = NSFetchRequest<TimelineMO>(entityName: "Timeline")
request.predicate = NSPredicate(format: "name == %@", "Home")
do {
let results = try CoreDataManager.shared.context.fetch(request)
session.timeline = results.first
CoreDataManager.shared.saveContext()
} catch {
print("\(error)")
}
}
guard let timeline = session.timeline else {
completion(nil)
return
}
loading = true
var request: Request<[Status]>
switch timeline.name {
case "Home":
request = Timelines.home(range: requestRange)
case "Local":
request = Timelines.public(local: true, range: requestRange)
case "Federated":
request = Timelines.public(local: false, range: requestRange)
case "Favorites":
request = Favourites.all(range: requestRange)
case let tag:
request = Timelines.tag(tag!, range: requestRange)
}
fetchStatuses(request: request, forTimeline: timeline) { statuses, error in
guard error == nil else {
completion(error)
return
}
let newStatuses = statuses.filter { $0.new }
if newStatuses.count > -1 {
DispatchQueue.main.async {
if let navigationController = self.navigationController as? TimelinesNavigationController,
let newStatusesView = navigationController.newStatusesView {
newStatusesView.setCount(newStatuses.count)
newStatusesView.isHidden = false
self.view.layoutIfNeeded()
UIView.animate(withDuration: 2.0, animations: { () -> Void in
navigationController.bottomLayoutConstraint?.constant = -20
self.view.layoutIfNeeded()
})
}
}
}
completion(nil)
}
}
}
extension TimelineTableViewController: NSFetchedResultsControllerDelegate {
func initializeFetchedResultsController() {
guard let timeline = AuthenticationManager.shared.session?.timeline else {
return
}
let request = NSFetchRequest<StatusMO>(entityName: "Status")
request.predicate = NSPredicate(format: "ANY timelines = %@", timeline)
request.sortDescriptors = [
NSSortDescriptor(key: "createdAt", ascending: false),
]
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: CoreDataManager.shared.context,
sectionNameKeyPath: nil,
cacheName: nil
)
fetchedResultsController!.delegate = self
do {
try fetchedResultsController!.performFetch()
} catch {
print("\(error)")
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.reloadData()
}
}
extension TimelineTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = fetchedResultsController?.fetchedObjects?.count else {
return 0
}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TimelineTableViewCell", for: indexPath) as? TimelineTableViewCell else {
fatalError("Unable to find reusable cell")
}
guard let status = fetchedResultsController?.object(at: indexPath), let statuses = fetchedResultsController?.fetchedObjects else {
fatalError("CoreData error")
}
guard let timeline = AuthenticationManager.shared.session?.timeline else {
fatalError("No timeline")
}
cell.statusView.update(withStatus: status)
let timelinePredicate = NSPredicate(format: "timeline = %@", timeline)
if let boundary = status.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if indexPath.row != 0 {
if boundary.start {
let previousStatus = statuses[indexPath.row - 1]
if let previousBoundary = previousStatus.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if !previousBoundary.start {
cell.statusView.topDividerView.isHidden = true
cell.statusView.topLoadMoreView.isHidden = false
}
}
}
} else {
cell.statusView.topDividerView.isHidden = true
}
if indexPath.row < statuses.count - 1 {
if !boundary.start {
let nextStatus = statuses[indexPath.row + 1]
if let nextBoundary = nextStatus.boundaries?.filtered(using: timelinePredicate).first as? TimelineBoundaryMO {
if nextBoundary.start {
cell.statusView.bottomDividerView.isHidden = true
cell.statusView.bottomLoadMoreView.isHidden = false
}
}
}
}
if indexPath.row == statuses.count - 1 {
cell.statusView.bottomDividerView.isHidden = true
}
}
if indexPath.row == statuses.count - 2 && !loading {
fetchTimeline(withRange: .max(id: status.id!, limit: fetchLimit)) { error in
guard error == nil else {
return
}
}
}
return cell
}
}