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

390 lines
14 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
enum StatusType {
case ancestor, status, descendant
}
class StatusTableViewController: AbstractStatusTableViewController, UIGestureRecognizerDelegate {
public var status: StatusMO? = nil
var ancestors: [StatusMO] = []
var descendants: [StatusMO] = []
var feedbackGenerator: UINotificationFeedbackGenerator? = nil
override func viewDidLoad() {
super.viewDidLoad()
feedbackGenerator = UINotificationFeedbackGenerator()
navigationItem.title = "Toot"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadStatuses()
fetchStatuses { error in
if error != nil {
AlertManager.shared.show(message: error!.localizedDescription, category: .error)
}
}
}
@IBAction func accountViewTapped(_ sender: Any) {
self.accountTapped(account: status!.account!)
}
@IBAction func boostTapped(_ sender: Any) {
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: Any) {
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(status: status!)
}
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
self.loadStatuses()
completion(nil)
}
}
}
}
}
func loadStatuses() {
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: false),
]
ancestors = []
descendants = []
do {
let statuses = try CoreDataManager.shared.context.fetch(request)
statuses.forEach { s in
switch s.createdAt!.compare(status.createdAt!) {
case .orderedAscending:
ancestors.append(s)
case .orderedSame:
return
case .orderedDescending:
descendants.append(s)
}
}
ancestors.reverse()
descendants.reverse()
self.tableView.reloadData()
} catch {
print("\(error)")
}
}
}
extension StatusTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
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 {
switch indexPath.section {
case 0: return CellHeightManager.get(status: self.ancestors[indexPath.row])
case 2: return CellHeightManager.get(status: self.descendants[indexPath.row])
default: return UITableView.automaticDimension
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let height = CGFloat(25)
switch section {
case 0: return ancestors.count > 0 ? height : 0
case 2: return descendants.count > 0 ? height : 0
default: return height
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return ancestors.count > 0 ? "In Reply To" : nil
case 2: return descendants.count > 0 ? "Replies" : nil
default: return "Toot"
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return ancestors.count
case 2: return descendants.count
default: return 1
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var status: StatusMO
switch indexPath.section {
case 0: status = self.ancestors[indexPath.row]
case 2: status = self.descendants[indexPath.row]
default: status = self.status!
}
if status == self.status {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MainStatusTableViewCell", for: indexPath) as? MainStatusTableViewCell else {
fatalError("Unable to find reusable cell")
}
cell.contentTextView.delegate = self
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)
}
cell.attachmentsView.backgroundColor = UIColor.white
cell.attachmentsView.isHidden = true
if let attachments = status.attachments, attachments.count > 0 {
cell.attachmentsView.isHidden = false
cell.attachmentsView.isUserInteractionEnabled = true
let attachmentManager = AttachmentManager()
attachmentManager.delegate = self
attachmentManager.setupAttachmentView(cell.attachmentsView, withAttachments: attachments)
}
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")
}
return cell
} else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "StatusTableViewCell", for: indexPath) as? StatusTableViewCell else {
fatalError("Unable to find reusable cell")
}
cell.statusView.delegate = self
cell.statusView.update(withStatus: status)
cell.statusView.topDividerView.isHidden = indexPath.row == 0
cell.statusView.bottomDividerView.isHidden = false
cell.statusView.topLoadMoreView.isHidden = true
cell.statusView.bottomLoadMoreView.isHidden = true
cell.statusView.replyView.isHidden = true
return cell
}
}
}
extension StatusTableViewController {
func boostTapped() {
loadStatuses()
}
func favoriteTapped() {
loadStatuses()
}
func revealTapped() {
loadStatuses()
}
func hideTapped() {
loadStatuses()
}
}
extension StatusTableViewController: AttachmentManagerDelegate {
func attachmentTapped(index: Int) {
self.attachmentTapped(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(account: account)
}
return false
}
}
}
self.urlTapped(url: URL)
return false
}
}