// // StatusTableViewController.swift // elpha-ios // // Created by Dwayne Harris on 10/15/18. // Copyright © 2018 Elpha. All rights reserved. // import CoreData import Kingfisher import UIKit import SafariServices class StatusTableViewController: AbstractStatusTableViewController, UIGestureRecognizerDelegate { public var status: StatusMO? var feedbackGenerator: UINotificationFeedbackGenerator? override func viewDidLoad() { super.viewDidLoad() feedbackGenerator = UINotificationFeedbackGenerator() navigationItem.title = "Toot" refreshControl?.addTarget(self, action: #selector(self.fetch), for: .valueChanged) initializeFetchedResultsController() self.fetch() } func scrollToMainStatusRow() { if let indexPath = self.fetchedResultsController?.indexPath(forObject: self.status!) { self.tableView.scrollToRow(at: indexPath, at: .top, animated: true) } } @objc func fetch() { fetchStatuses { error in guard error == nil else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.scrollToMainStatusRow() } } } @IBAction func accountViewTapped(_ sender: Any) { self.accountTapped(self, account: status!.account!) } @IBAction func boostTapped(_ sender: UIView) { if let status = status { feedbackGenerator?.prepare() if status.reblogged { status.reblogged = false status.reblogsCount = status.reblogsCount - 1 MastodonAPI.unreblog(statusID: status.id!) { error in guard error == nil else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) self.feedbackGenerator?.notificationOccurred(.error) return } AlertManager.shared.show(message: "Unboosted", category: .boosted) self.feedbackGenerator?.notificationOccurred(.success) } } else { status.reblogged = true status.reblogsCount = status.reblogsCount + 1 MastodonAPI.reblog(statusID: status.id!) { error in guard error == nil else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) self.feedbackGenerator?.notificationOccurred(.error) return } AlertManager.shared.show(message: "Boosted!", category: .boosted) self.feedbackGenerator?.notificationOccurred(.success) } } CoreDataManager.shared.saveContext() } } @IBAction func favoriteTapped(_ sender: UIView) { if let status = status { feedbackGenerator?.prepare() if status.favorited { status.favorited = false status.favoritesCount = status.favoritesCount - 1 MastodonAPI.unfavorite(statusID: status.id!) { error in guard error == nil else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) self.feedbackGenerator?.notificationOccurred(.error) return } AlertManager.shared.show(message: "Unfavorited", category: .favorited) self.feedbackGenerator?.notificationOccurred(.success) } } else { status.favorited = true status.favoritesCount = status.favoritesCount + 1 MastodonAPI.favorite(statusID: status.id!) { error in guard error == nil else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) self.feedbackGenerator?.notificationOccurred(.error) return } AlertManager.shared.show(message: "Favorited!", category: .favorited) self.feedbackGenerator?.notificationOccurred(.success) } } CoreDataManager.shared.saveContext() } } @IBAction func replyTapped(_ sender: Any) { self.replyTapped(self, status: status!) } @IBAction func shareTapped(_ sender: Any) { if let status = status { let controller = UIActivityViewController(activityItems: [status.uri!], applicationActivities: nil) present(controller, animated: true) } } func fetchStatuses(completion: @escaping (Error?) -> Void) { if let status = status { loading = true MastodonAPI.status(id: status.id!) { data, error in guard let data = data, error == nil else { completion(error) return } DispatchQueue.main.async { _ = MastodonDataManager.upsertStatus(data) } MastodonAPI.context(id: status.id!) { data, error in guard let data = data, error == nil else { completion(error) return } DispatchQueue.main.async { if let ancestors = data["ancestors"] as? [JSONObject] { ancestors.forEach { ancestor in let ancestor = MastodonDataManager.upsertStatus(ancestor) status.addToAncestors(ancestor!.object) } } if let descendant = data["descendants"] as? [JSONObject] { descendant.forEach { descendant in let descendant = MastodonDataManager.upsertStatus(descendant) status.addToDescendants(descendant!.object) } } CoreDataManager.shared.saveContext() self.loading = false completion(nil) } } } } } override func updateCell(_ cell: AbstractStatusTableViewCell, withStatusAt indexPath: IndexPath) { guard let status = fetchedResultsController?.object(at: indexPath) else { fatalError("CoreData error") } cell.statusView.delegate = self cell.statusView.update(withStatus: status, excludeReplyView: true) cell.statusView.topDividerView.isHidden = indexPath.row == 0 cell.statusView.bottomDividerView.isHidden = false cell.statusView.topLoadMoreView.isHidden = true cell.statusView.bottomLoadMoreView.isHidden = true let statusCount = fetchedResultsController?.fetchedObjects?.count ?? 0 if indexPath.row == statusCount - 1 { cell.statusView.bottomDividerView.isHidden = true } } func updateMainCell(_ cell: MainStatusTableViewCell, withStatusAt indexPath: IndexPath) { guard let status = status else { return } cell.avatarImageView.setRoundedCorners() func updateAccountView(status: StatusMO) { if let account = status.account { let options: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame] cell.avatarImageView.kf.setImage(with: account.avatarURL!, options: options) cell.displayNameLabel.text = account.displayName cell.usernameLabel.text = "@\(account.acct!)" } } if let reblog = status.reblog { updateAccountView(status: reblog) } else { updateAccountView(status: status) } if let content = status.content { cell.contentTextView.attributedText = content.htmlAttributed(size: 16) } if let attachments = status.attachments, attachments.count > 0 { cell.attachmentsView.isHidden = false cell.attachmentsView.delegate = self cell.attachmentsView.update(withAttachments: attachments.array as! [AttachmentMO]) } else { cell.attachmentsView.isHidden = true } let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .none cell.timestampDateLabel.text = dateFormatter.string(from: status.createdAt!) dateFormatter.dateStyle = .none dateFormatter.timeStyle = .short cell.timestampTimeLabel.text = dateFormatter.string(from: status.createdAt!) cell.repliesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.repliesCount), number: .decimal) cell.boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal) cell.favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favoritesCount), number: .decimal) if status.reblogged { cell.boostsImageView.image = UIImage(named: "Boost Bold") } else { cell.boostsImageView.image = UIImage(named: "Boost Regular") } if status.favorited { cell.favoritesImageView.image = UIImage(named: "Star Filled") } else { cell.favoritesImageView.image = UIImage(named: "Star Regular") } if let ancestors = status.ancestors, ancestors.count > 0 { cell.inReplyToView.isHidden = false } else { cell.inReplyToView.isHidden = true } if let descendants = status.descendants, descendants.count > 0 { cell.repliesView.isHidden = false } else { cell.repliesView.isHidden = true } } } extension StatusTableViewController: NSFetchedResultsControllerDelegate { func initializeFetchedResultsController() { guard let status = status else { return } let request = NSFetchRequest(entityName: "Status") request.predicate = NSPredicate(format: "id == %@ OR ANY ancestors == %@ OR ANY descendants == %@", status.id!, status, status) request.sortDescriptors = [ NSSortDescriptor(key: "createdAt", ascending: true), ] 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: switch tableView.cellForRow(at: indexPath!) { case let cell as StatusTableViewCell: updateCell(cell, withStatusAt: indexPath!) case let cell as MainStatusTableViewCell: self.status = anObject as? StatusMO updateMainCell(cell, withStatusAt: indexPath!) default: return } 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 StatusTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { return 1 } // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // if let cell = cell as? StatusTableViewCell { // CellHeightManager.set(status: cell.statusView.status, height: cell.frame.size.height) // } // } // override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { // guard let status = fetchedResultsController?.object(at: indexPath), status != self.status else { // return UITableView.automaticDimension // } // // return CellHeightManager.get(status: status) // } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fetchedResultsController?.fetchedObjects?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let status = fetchedResultsController?.object(at: indexPath) else { fatalError("CoreData error") } if status == self.status { guard let cell = tableView.dequeueReusableCell(withIdentifier: "MainStatusTableViewCell", for: indexPath) as? MainStatusTableViewCell else { fatalError("Unable to find reusable cell") } updateMainCell(cell, withStatusAt: indexPath) cell.contentTextView.delegate = self return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: "StatusTableViewCell", for: indexPath) as? StatusTableViewCell else { fatalError("Unable to find reusable cell") } updateCell(cell, withStatusAt: indexPath) cell.statusView.delegate = self return cell } } } extension StatusTableViewController: AttachmentsViewDelegate { func attachmentTapped(_ sender: Any, index: Int) { self.attachmentTapped(self, status: status!, index: index) } } extension StatusTableViewController: UITextViewDelegate { func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { if let mentions = status?.mentions { for mention in mentions { if URL == mention.url { if let account = MastodonDataManager.account(id: mention.id) { self.accountTapped(self, account: account) } return false } } } if URL.scheme == "http" || URL.scheme == "https" { self.urlTapped(self, url: URL) } return false } }