[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

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
4 years ago
6 years ago
4 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. //
  2. // StatusTableViewController.swift
  3. // elpha-ios
  4. //
  5. // Created by Dwayne Harris on 10/15/18.
  6. // Copyright © 2018 Elpha. All rights reserved.
  7. //
  8. import CoreData
  9. import Kingfisher
  10. import UIKit
  11. import SafariServices
  12. class StatusTableViewController: AbstractStatusTableViewController, UIGestureRecognizerDelegate {
  13. public var status: StatusMO?
  14. var feedbackGenerator: UINotificationFeedbackGenerator?
  15. override func viewDidLoad() {
  16. super.viewDidLoad()
  17. feedbackGenerator = UINotificationFeedbackGenerator()
  18. navigationItem.title = "Toot"
  19. refreshControl?.addTarget(self, action: #selector(self.fetch), for: .valueChanged)
  20. initializeFetchedResultsController()
  21. self.fetch()
  22. }
  23. func scrollToMainStatusRow() {
  24. if let indexPath = self.fetchedResultsController?.indexPath(forObject: self.status!) {
  25. self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
  26. }
  27. }
  28. @objc func fetch() {
  29. fetchStatuses { error in
  30. guard error == nil else {
  31. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  32. return
  33. }
  34. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  35. self.scrollToMainStatusRow()
  36. }
  37. }
  38. }
  39. @IBAction func accountViewTapped(_ sender: Any) {
  40. self.accountTapped(self, account: status!.account!)
  41. }
  42. @IBAction func boostTapped(_ sender: UIView) {
  43. if let status = status {
  44. feedbackGenerator?.prepare()
  45. if status.reblogged {
  46. status.reblogged = false
  47. status.reblogsCount = status.reblogsCount - 1
  48. MastodonAPI.unreblog(statusID: status.id!) { error in
  49. guard error == nil else {
  50. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  51. self.feedbackGenerator?.notificationOccurred(.error)
  52. return
  53. }
  54. AlertManager.shared.show(message: "Unboosted", category: .boosted)
  55. self.feedbackGenerator?.notificationOccurred(.success)
  56. }
  57. } else {
  58. status.reblogged = true
  59. status.reblogsCount = status.reblogsCount + 1
  60. MastodonAPI.reblog(statusID: status.id!) { error in
  61. guard error == nil else {
  62. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  63. self.feedbackGenerator?.notificationOccurred(.error)
  64. return
  65. }
  66. AlertManager.shared.show(message: "Boosted!", category: .boosted)
  67. self.feedbackGenerator?.notificationOccurred(.success)
  68. }
  69. }
  70. CoreDataManager.shared.saveContext()
  71. }
  72. }
  73. @IBAction func favoriteTapped(_ sender: UIView) {
  74. if let status = status {
  75. feedbackGenerator?.prepare()
  76. if status.favorited {
  77. status.favorited = false
  78. status.favoritesCount = status.favoritesCount - 1
  79. MastodonAPI.unfavorite(statusID: status.id!) { error in
  80. guard error == nil else {
  81. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  82. self.feedbackGenerator?.notificationOccurred(.error)
  83. return
  84. }
  85. AlertManager.shared.show(message: "Unfavorited", category: .favorited)
  86. self.feedbackGenerator?.notificationOccurred(.success)
  87. }
  88. } else {
  89. status.favorited = true
  90. status.favoritesCount = status.favoritesCount + 1
  91. MastodonAPI.favorite(statusID: status.id!) { error in
  92. guard error == nil else {
  93. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  94. self.feedbackGenerator?.notificationOccurred(.error)
  95. return
  96. }
  97. AlertManager.shared.show(message: "Favorited!", category: .favorited)
  98. self.feedbackGenerator?.notificationOccurred(.success)
  99. }
  100. }
  101. CoreDataManager.shared.saveContext()
  102. }
  103. }
  104. @IBAction func replyTapped(_ sender: Any) {
  105. self.replyTapped(self, status: status!)
  106. }
  107. @IBAction func shareTapped(_ sender: Any) {
  108. if let status = status {
  109. let controller = UIActivityViewController(activityItems: [status.uri!], applicationActivities: nil)
  110. present(controller, animated: true)
  111. }
  112. }
  113. func fetchStatuses(completion: @escaping (Error?) -> Void) {
  114. if let status = status {
  115. loading = true
  116. MastodonAPI.status(id: status.id!) { data, error in
  117. guard let data = data, error == nil else {
  118. completion(error)
  119. return
  120. }
  121. DispatchQueue.main.async {
  122. _ = MastodonDataManager.upsertStatus(data)
  123. }
  124. MastodonAPI.context(id: status.id!) { data, error in
  125. guard let data = data, error == nil else {
  126. completion(error)
  127. return
  128. }
  129. DispatchQueue.main.async {
  130. if let ancestors = data["ancestors"] as? [JSONObject] {
  131. ancestors.forEach { ancestor in
  132. let ancestor = MastodonDataManager.upsertStatus(ancestor)
  133. status.addToAncestors(ancestor!.object)
  134. }
  135. }
  136. if let descendant = data["descendants"] as? [JSONObject] {
  137. descendant.forEach { descendant in
  138. let descendant = MastodonDataManager.upsertStatus(descendant)
  139. status.addToDescendants(descendant!.object)
  140. }
  141. }
  142. CoreDataManager.shared.saveContext()
  143. self.loading = false
  144. completion(nil)
  145. }
  146. }
  147. }
  148. }
  149. }
  150. override func updateCell(_ cell: AbstractStatusTableViewCell, withStatusAt indexPath: IndexPath) {
  151. guard let status = fetchedResultsController?.object(at: indexPath) else {
  152. fatalError("CoreData error")
  153. }
  154. cell.statusView.delegate = self
  155. cell.statusView.update(withStatus: status, excludeReplyView: true)
  156. cell.statusView.topDividerView.isHidden = indexPath.row == 0
  157. cell.statusView.bottomDividerView.isHidden = false
  158. cell.statusView.topLoadMoreView.isHidden = true
  159. cell.statusView.bottomLoadMoreView.isHidden = true
  160. let statusCount = fetchedResultsController?.fetchedObjects?.count ?? 0
  161. if indexPath.row == statusCount - 1 {
  162. cell.statusView.bottomDividerView.isHidden = true
  163. }
  164. }
  165. func updateMainCell(_ cell: MainStatusTableViewCell, withStatusAt indexPath: IndexPath) {
  166. guard let status = status else {
  167. return
  168. }
  169. cell.avatarImageView.setRoundedCorners()
  170. func updateAccountView(status: StatusMO) {
  171. if let account = status.account {
  172. let options: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame]
  173. cell.avatarImageView.kf.setImage(with: account.avatarURL!, options: options)
  174. cell.displayNameLabel.text = account.displayName
  175. cell.usernameLabel.text = "@\(account.acct!)"
  176. }
  177. }
  178. if let reblog = status.reblog {
  179. updateAccountView(status: reblog)
  180. } else {
  181. updateAccountView(status: status)
  182. }
  183. if let content = status.content {
  184. cell.contentTextView.attributedText = content.htmlAttributed(size: 16)
  185. }
  186. if let attachments = status.attachments, attachments.count > 0 {
  187. cell.attachmentsView.isHidden = false
  188. cell.attachmentsView.delegate = self
  189. cell.attachmentsView.update(withAttachments: attachments.array as! [AttachmentMO])
  190. } else {
  191. cell.attachmentsView.isHidden = true
  192. }
  193. let dateFormatter = DateFormatter()
  194. dateFormatter.dateStyle = .medium
  195. dateFormatter.timeStyle = .none
  196. cell.timestampDateLabel.text = dateFormatter.string(from: status.createdAt!)
  197. dateFormatter.dateStyle = .none
  198. dateFormatter.timeStyle = .short
  199. cell.timestampTimeLabel.text = dateFormatter.string(from: status.createdAt!)
  200. cell.repliesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.repliesCount), number: .decimal)
  201. cell.boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal)
  202. cell.favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favoritesCount), number: .decimal)
  203. if status.reblogged {
  204. cell.boostsImageView.image = UIImage(named: "Boost Bold")
  205. } else {
  206. cell.boostsImageView.image = UIImage(named: "Boost Regular")
  207. }
  208. if status.favorited {
  209. cell.favoritesImageView.image = UIImage(named: "Star Filled")
  210. } else {
  211. cell.favoritesImageView.image = UIImage(named: "Star Regular")
  212. }
  213. if let ancestors = status.ancestors, ancestors.count > 0 {
  214. cell.inReplyToView.isHidden = false
  215. } else {
  216. cell.inReplyToView.isHidden = true
  217. }
  218. if let descendants = status.descendants, descendants.count > 0 {
  219. cell.repliesView.isHidden = false
  220. } else {
  221. cell.repliesView.isHidden = true
  222. }
  223. }
  224. }
  225. extension StatusTableViewController: NSFetchedResultsControllerDelegate {
  226. func initializeFetchedResultsController() {
  227. guard let status = status else {
  228. return
  229. }
  230. let request = NSFetchRequest<StatusMO>(entityName: "Status")
  231. request.predicate = NSPredicate(format: "id == %@ OR ANY ancestors == %@ OR ANY descendants == %@", status.id!, status, status)
  232. request.sortDescriptors = [
  233. NSSortDescriptor(key: "createdAt", ascending: true),
  234. ]
  235. fetchedResultsController = NSFetchedResultsController(
  236. fetchRequest: request,
  237. managedObjectContext: CoreDataManager.shared.context,
  238. sectionNameKeyPath: nil,
  239. cacheName: nil
  240. )
  241. fetchedResultsController!.delegate = self
  242. do {
  243. try fetchedResultsController!.performFetch()
  244. } catch {
  245. print("\(error)")
  246. }
  247. }
  248. func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  249. tableView.beginUpdates()
  250. }
  251. func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
  252. switch type {
  253. case NSFetchedResultsChangeType.insert:
  254. tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.none)
  255. case NSFetchedResultsChangeType.delete:
  256. tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.none)
  257. case NSFetchedResultsChangeType.update:
  258. switch tableView.cellForRow(at: indexPath!) {
  259. case let cell as StatusTableViewCell:
  260. updateCell(cell, withStatusAt: indexPath!)
  261. case let cell as MainStatusTableViewCell:
  262. self.status = anObject as? StatusMO
  263. updateMainCell(cell, withStatusAt: indexPath!)
  264. default:
  265. return
  266. }
  267. case NSFetchedResultsChangeType.move:
  268. tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade)
  269. tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade)
  270. }
  271. }
  272. func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  273. tableView.endUpdates()
  274. }
  275. }
  276. extension StatusTableViewController {
  277. override func numberOfSections(in tableView: UITableView) -> Int {
  278. return 1
  279. }
  280. // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  281. // if let cell = cell as? StatusTableViewCell {
  282. // CellHeightManager.set(status: cell.statusView.status, height: cell.frame.size.height)
  283. // }
  284. // }
  285. // override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  286. // guard let status = fetchedResultsController?.object(at: indexPath), status != self.status else {
  287. // return UITableView.automaticDimension
  288. // }
  289. //
  290. // return CellHeightManager.get(status: status)
  291. // }
  292. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  293. return fetchedResultsController?.fetchedObjects?.count ?? 0
  294. }
  295. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  296. guard let status = fetchedResultsController?.object(at: indexPath) else {
  297. fatalError("CoreData error")
  298. }
  299. if status == self.status {
  300. guard let cell = tableView.dequeueReusableCell(withIdentifier: "MainStatusTableViewCell", for: indexPath) as? MainStatusTableViewCell else {
  301. fatalError("Unable to find reusable cell")
  302. }
  303. updateMainCell(cell, withStatusAt: indexPath)
  304. cell.contentTextView.delegate = self
  305. return cell
  306. } else {
  307. guard let cell = tableView.dequeueReusableCell(withIdentifier: "StatusTableViewCell", for: indexPath) as? StatusTableViewCell else {
  308. fatalError("Unable to find reusable cell")
  309. }
  310. updateCell(cell, withStatusAt: indexPath)
  311. cell.statusView.delegate = self
  312. return cell
  313. }
  314. }
  315. }
  316. extension StatusTableViewController: AttachmentsViewDelegate {
  317. func attachmentTapped(_ sender: Any, index: Int) {
  318. self.attachmentTapped(self, status: status!, index: index)
  319. }
  320. }
  321. extension StatusTableViewController: UITextViewDelegate {
  322. func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
  323. if let mentions = status?.mentions {
  324. for mention in mentions {
  325. if URL == mention.url {
  326. if let account = MastodonDataManager.account(id: mention.id) {
  327. self.accountTapped(self, account: account)
  328. }
  329. return false
  330. }
  331. }
  332. }
  333. if URL.scheme == "http" || URL.scheme == "https" {
  334. self.urlTapped(self, url: URL)
  335. }
  336. return false
  337. }
  338. }