[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.

415 lines
16 KiB

//
// 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<StatusMO>(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<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, 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<NSFetchRequestResult>) {
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
}
}