// // TimelineTableViewController.swift // elpha-ios // // Created by Dwayne Harris on 10/1/18. // Copyright © 2018 Elpha. All rights reserved. // import CoreData import Kingfisher import UIKit import SafariServices class TimelineTableViewController: AbstractStatusTableViewController { var feedbackGenerator: UINotificationFeedbackGenerator? override var currentPaginationContext: String { get { guard let timeline = AuthenticationManager.session?.timeline, let categoryString = timeline.category else { return "" } switch TimelineCategory(rawValue: categoryString)! { case .home: return "timeline:home" case .local: return "timeline:local" case .federated: return "timeline:federated" case .tag: return "timeline:tag:\(timeline.subcategory!)" case .favorites: return "timeline:favorites" } } } override func viewDidLoad() { super.viewDidLoad() feedbackGenerator = UINotificationFeedbackGenerator() initializeFetchedResultsController() let scale = UIScreen.main.scale let avatarButtonSize: CGFloat = 30 let avatarButtonScaledSize = avatarButtonSize * scale let moreButtonItem = UIBarButtonItem(image: UIImage(named: "More"), style: .plain, target: self, action: #selector(more)) let composeButtonItem = UIBarButtonItem(image: UIImage(named: "Compose"), style: .plain, target: self, action: #selector(compose)) let avatarButton = UIButton() avatarButton.setBackgroundImage(UIImage(named: "Account"), for: .normal) avatarButton.addTarget(self, action: #selector(self.openSettings), for: .touchUpInside) NSLayoutConstraint.activate([ avatarButton.widthAnchor.constraint(equalToConstant: avatarButtonSize), avatarButton.heightAnchor.constraint(equalToConstant: avatarButtonSize) ]) let avatarButtonItem = UIBarButtonItem(customView: avatarButton) navigationItem.leftBarButtonItems = [avatarButtonItem, moreButtonItem] navigationItem.rightBarButtonItems = [composeButtonItem] if let account = AuthenticationManager.session?.account { let processor = ResizingImageProcessor(referenceSize: CGSize(width: avatarButtonScaledSize, height: avatarButtonScaledSize), mode: .aspectFill) >> RoundCornerImageProcessor(cornerRadius: avatarButtonScaledSize / 2) ImageDownloader.default.downloadImage(with: account.avatarURL!, retrieveImageTask: nil, options: [.processor(processor)], progressBlock: nil) { image, error, url, data in if let image = image { avatarButton.setBackgroundImage(image.withRenderingMode(.alwaysOriginal), for: .normal) } } } if let timeline = AuthenticationManager.session?.timeline { navigationItem.title = timeline.name } else { navigationItem.title = "Home" } refreshControl?.addTarget(self, action: #selector(self.fetch), for: .valueChanged) NotificationCenter.default.addObserver(self, selector: #selector(onDidAuthenticate), name: .didAuthenticate, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(onDidUnauthenticate), name: .didUnauthenticate, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(onDidBecomeActive), name: .didBecomeActive, object: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if SettingsManager.automaticallyRefreshTimelines { fetch() } } override func loadMoreTapped(_ sender: Any, status: StatusMO, direction: PaginationDirection) { var markerIndex = -1 if let markers = status.markers { for (index, marker) in markers.enumerated() { if marker.context == self.currentPaginationContext && marker.item.direction == direction { markerIndex = index } } if markerIndex > -1 { status.markers?.remove(at: markerIndex) CoreDataManager.shared.saveContext() fetchTimeline(withPagination: markers[markerIndex].item) { error in if error != nil { AlertManager.shared.show(message: error!.localizedDescription, category: .error) } } } } } @objc func openSettings() { let storyboard = UIStoryboard(name: "Main", bundle: nil) if let controller = storyboard.instantiateViewController(withIdentifier: "SettingsTableViewController") as? SettingsTableViewController { self.navigationController?.pushViewController(controller, animated: true) } } @objc func more() { performSegue(withIdentifier: "TimelinesSegue", sender: self) } @objc func compose() { let storyboard = UIStoryboard(name: "Main", bundle: nil) if let controller = storyboard.instantiateViewController(withIdentifier: "ComposeViewController") as? ComposeViewController { present(controller, animated: true) } } @objc func onDidAuthenticate(_ notification: Notification) { initializeFetchedResultsController() tableView.reloadData() fetch() } @objc func onDidUnauthenticate(_ notification: Notification) { performSegue(withIdentifier: "AuthenticateSegue", sender: self) } @objc func onDidBecomeActive() { if SettingsManager.automaticallyRefreshTimelines { fetch() } } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "TimelinesSegue" { if let destination = segue.destination as? TimelinesViewController { destination.delegate = self } } } func createDefaultTimelines(account: AccountMO) { let homeTimeline = TimelineMO(context: CoreDataManager.shared.context) homeTimeline.name = "Home" homeTimeline.category = TimelineCategory.home.rawValue homeTimeline.account = account homeTimeline.order = 1 let localTimeline = TimelineMO(context: CoreDataManager.shared.context) localTimeline.name = "Local" localTimeline.category = TimelineCategory.local.rawValue localTimeline.account = account localTimeline.order = 2 let federatedTimeline = TimelineMO(context: CoreDataManager.shared.context) federatedTimeline.name = "Federated" federatedTimeline.category = TimelineCategory.federated.rawValue federatedTimeline.account = account federatedTimeline.order = 3 let favoritesTimeline = TimelineMO(context: CoreDataManager.shared.context) favoritesTimeline.name = "Favorites" favoritesTimeline.category = TimelineCategory.favorites.rawValue favoritesTimeline.account = account favoritesTimeline.order = 4 CoreDataManager.shared.saveContext() } @objc func fetch() { fetchTimeline { error in if error != nil { AlertManager.shared.show(message: error!.localizedDescription, category: .error) } } } func fetchTimeline(withPagination pagination: PaginationItem? = nil, completion: @escaping (Error?) -> Void) { guard let session = AuthenticationManager.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(entityName: "Timeline") request.predicate = NSPredicate(format: "category == %@", TimelineCategory.home.rawValue) 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 } func requestCompletion(data: [JSONObject]?, pagination: [PaginationItem]?, error: Error?) { guard let data = data, error == nil else { completion(error) return } DispatchQueue.main.async { var newStatusCount = 0 self.feedbackGenerator?.prepare() for (index, status) in data.enumerated() { if let upsertResult = MastodonDataManager.upsertStatus(status) { let status = upsertResult.object if upsertResult.new { newStatusCount = newStatusCount + 1 } if let pagination = pagination { var markers: [PaginationMarker] = status.markers ?? [] markers = Array(markers.drop { $0.context == self.currentPaginationContext }) if index == 0 { pagination.forEach { item in if item.direction == .prev { markers.append(PaginationMarker(context: self.currentPaginationContext, item: item)) } } } if index == data.count - 1 { pagination.forEach { item in if item.direction == .next { markers.append(PaginationMarker(context: self.currentPaginationContext, item: item)) } } } status.markers = markers } timeline.addToStatuses(status) } } if newStatusCount > 0 { let pluralization = newStatusCount == 1 ? "" : "s" AlertManager.shared.show(message: "\(newStatusCount) new toot\(pluralization)", category: .newStatuses) self.feedbackGenerator?.notificationOccurred(.success) } CoreDataManager.shared.saveContext() self.loading = false completion(nil) } } loading = true switch TimelineCategory(rawValue: timeline.category!)! { case .home: MastodonAPI.homeTimeline( limit: fetchLimit, pagination: pagination, completion: requestCompletion ) case .local: MastodonAPI.publicTimeline( local: true, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) case .federated: MastodonAPI.publicTimeline( local: false, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) case .tag: MastodonAPI.tagTimeline( tag: timeline.subcategory!, local: false, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) case .favorites: MastodonAPI.favorites( limit: fetchLimit, pagination: pagination, completion: requestCompletion ) } } } extension TimelineTableViewController: NSFetchedResultsControllerDelegate { func initializeFetchedResultsController() { guard let timeline = AuthenticationManager.session?.timeline else { return } let request = NSFetchRequest(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 controllerWillChangeContent(_ controller: NSFetchedResultsController) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case NSFetchedResultsChangeType.insert: tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.none) case NSFetchedResultsChangeType.delete: tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.none) case NSFetchedResultsChangeType.update: if let cell = tableView.cellForRow(at: indexPath!) as? TimelineTableViewCell { updateCell(cell, withStatusAt: indexPath!) } case NSFetchedResultsChangeType.move: tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade) tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade) } } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { tableView.endUpdates() } } extension TimelineTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fetchedResultsController?.fetchedObjects?.count ?? 0 } // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // if let cell = cell as? TimelineTableViewCell { // CellHeightManager.set(status: cell.statusView.status, height: cell.frame.size.height) // } // } // override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { // return CellHeightManager.get(status: fetchedResultsController?.object(at: indexPath)) // } 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") } updateCell(cell, withStatusAt: indexPath) cell.statusView.delegate = self return cell } } extension TimelineTableViewController: TimelinesViewControllerDelegate { func didSelect(_ sender: Any, timeline: TimelineMO) { navigationItem.title = timeline.name AuthenticationManager.session?.timeline = timeline CoreDataManager.shared.saveContext() initializeFetchedResultsController() tableView.reloadData() if SettingsManager.automaticallyRefreshTimelines { fetch() } } }