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

402 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
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
  1. //
  2. // AccountTableView.swift
  3. // elpha-ios
  4. //
  5. // Created by Dwayne Harris on 10/9/18.
  6. // Copyright © 2018 Elpha. All rights reserved.
  7. //
  8. import CoreData
  9. import Kingfisher
  10. import UIKit
  11. import SafariServices
  12. class FieldTableViewController: NSObject, UITableViewDelegate, UITableViewDataSource {
  13. var fields: [AccountField]?
  14. func numberOfSections(in tableView: UITableView) -> Int {
  15. return 1
  16. }
  17. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  18. return fields?.count ?? 0
  19. }
  20. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  21. guard let cell = tableView.dequeueReusableCell(withIdentifier: "FieldTableViewCell", for: indexPath) as? FieldTableViewCell else {
  22. fatalError("Unable to find reusable cell")
  23. }
  24. if let fields = fields {
  25. let field = fields[indexPath.row]
  26. cell.nameLabel.text = field.name
  27. do {
  28. let htmlString = """
  29. <style>
  30. html * {
  31. font-family: -apple-system !important;
  32. font-size: 12px !important;
  33. font-weight: bold;
  34. color: \(UIColor(named: "Text")!.hexString()) !important;
  35. text-align: right !important;
  36. }
  37. </style>
  38. \(field.value)
  39. """
  40. if let data = htmlString.data(using: String.Encoding.utf16) {
  41. cell.valueTextView.attributedText = try NSAttributedString(
  42. data: data,
  43. options: [.documentType: NSAttributedString.DocumentType.html],
  44. documentAttributes: nil
  45. )
  46. }
  47. } catch {
  48. print("\(error)")
  49. }
  50. }
  51. return cell
  52. }
  53. }
  54. class AccountTableViewController: AbstractStatusTableViewController {
  55. @IBOutlet var headerView: UIView!
  56. @IBOutlet var headerImageView: UIImageView!
  57. @IBOutlet var avatarImageView: UIImageView!
  58. @IBOutlet var displayNameLabel: UILabel!
  59. @IBOutlet var usernameLabel: UILabel!
  60. @IBOutlet var statusesLabel: UILabel!
  61. @IBOutlet var followingLabel: UILabel!
  62. @IBOutlet var followersLabel: UILabel!
  63. @IBOutlet var statusTypeSegmentedControl: UISegmentedControl!
  64. @IBOutlet var contentTextView: UITextViewFixed!
  65. @IBOutlet var fieldTableView: UITableView!
  66. var account: AccountMO?
  67. var fieldTableViewController: FieldTableViewController?
  68. override var currentPaginationContext: String {
  69. get {
  70. guard let account = self.account else {
  71. return ""
  72. }
  73. return "account:\(account.acct!):\(statusTypeSegmentedControl.selectedSegmentIndex)"
  74. }
  75. }
  76. @IBAction func statusTypeChanged(_ sender: UISegmentedControl) {
  77. initializeFetchedResultsController()
  78. tableView.reloadData()
  79. fetch()
  80. }
  81. override func viewDidLoad() {
  82. super.viewDidLoad()
  83. refreshControl?.addTarget(self, action: #selector(self.fetch), for: .valueChanged)
  84. fieldTableViewController = FieldTableViewController()
  85. avatarImageView.setRoundedCorners()
  86. avatarImageView.setShadow()
  87. if self.account == nil {
  88. if let session = AuthenticationManager.session {
  89. self.account = session.account
  90. }
  91. }
  92. if let account = account {
  93. updateHeader(withAccount: account)
  94. // sizeHeaderToFit()
  95. initializeFetchedResultsController()
  96. }
  97. }
  98. override func viewDidAppear(_ animated: Bool) {
  99. super.viewDidAppear(animated)
  100. if let account = self.account {
  101. MastodonAPI.account(id: account.id!) { data, error in
  102. guard let data = data, error == nil else {
  103. return
  104. }
  105. self.account = MastodonDataManager.upsertAccount(data)
  106. if let account = self.account {
  107. DispatchQueue.main.async {
  108. self.updateHeader(withAccount: account)
  109. self.fetch()
  110. }
  111. }
  112. }
  113. }
  114. }
  115. private func sizeHeaderToFit() {
  116. headerView.translatesAutoresizingMaskIntoConstraints = false
  117. let headerWidth = headerView.bounds.size.width
  118. let headerWidthConstraints = NSLayoutConstraint.constraints(withVisualFormat: "[headerView(width)]", options: NSLayoutConstraint.FormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView])
  119. headerView.addConstraints(headerWidthConstraints)
  120. headerView.setNeedsLayout()
  121. headerView.layoutIfNeeded()
  122. let headerSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
  123. let height = headerSize.height
  124. var frame = headerView.frame
  125. frame.size.height = height
  126. headerView.frame = frame
  127. headerView.removeConstraints(headerWidthConstraints)
  128. headerView.translatesAutoresizingMaskIntoConstraints = true
  129. }
  130. private func updateHeader(withAccount account: AccountMO) {
  131. navigationItem.title = account.displayName
  132. if let headerURL = account.headerURL {
  133. headerImageView.kf.setImage(with: headerURL)
  134. }
  135. if let avatarURL = account.avatarURL {
  136. let options: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame]
  137. avatarImageView.kf.setImage(with: avatarURL, options: options)
  138. }
  139. displayNameLabel.text = account.displayName
  140. usernameLabel.text = "@\(account.acct!)"
  141. if let note = account.note {
  142. contentTextView.attributedText = note.htmlAttributed(size: 15, centered: true)
  143. }
  144. statusesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.statusesCount), number: .decimal)
  145. followingLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.followingCount), number: .decimal)
  146. followersLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.followersCount), number: .decimal)
  147. if let fields = account.fields, fields.count > 0 {
  148. fieldTableViewController!.fields = fields
  149. fieldTableView.dataSource = fieldTableViewController
  150. fieldTableView.delegate = fieldTableViewController
  151. fieldTableView.isHidden = false
  152. } else {
  153. fieldTableView.isHidden = true
  154. }
  155. }
  156. override func loadMoreTapped(_ sender: Any, status: StatusMO, direction: PaginationDirection) {
  157. func removeMarker(at: Int) {
  158. status.markers?.remove(at: at)
  159. CoreDataManager.shared.saveContext()
  160. }
  161. if let markers = status.markers {
  162. for (index, marker) in markers.enumerated() {
  163. if marker.context == self.currentPaginationContext && marker.item.direction == direction {
  164. fetchStatuses(withPagination: marker.item) { error in
  165. if error == nil {
  166. removeMarker(at: index)
  167. } else {
  168. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  169. }
  170. }
  171. }
  172. }
  173. }
  174. }
  175. @objc func fetch() {
  176. fetchStatuses { error in
  177. if error != nil {
  178. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  179. }
  180. }
  181. }
  182. func fetchStatuses(withPagination pagination: PaginationItem? = nil, completion: @escaping (Error?) -> Void) {
  183. if let account = account {
  184. func requestCompletion(data: JSONObjectArray?, pagination: [PaginationItem]?, error: Error?) {
  185. guard let data = data, error == nil else {
  186. self.loading = false
  187. completion(error)
  188. return
  189. }
  190. for (index, status) in data.enumerated() {
  191. if let upsertResult = MastodonDataManager.upsertStatus(status) {
  192. let status = upsertResult.object
  193. if let pagination = pagination {
  194. var markers: [PaginationMarker] = status.markers ?? []
  195. if index == 0 {
  196. pagination.forEach { item in
  197. if item.direction == .prev {
  198. markers.append(PaginationMarker(context: self.currentPaginationContext, item: item))
  199. }
  200. }
  201. }
  202. if index == data.count - 1 {
  203. pagination.forEach { item in
  204. if item.direction == .next {
  205. markers.append(PaginationMarker(context: self.currentPaginationContext, item: item))
  206. }
  207. }
  208. }
  209. status.markers = markers
  210. }
  211. }
  212. }
  213. MastodonAPI.pinnedStatuses(accountID: account.id!, limit: fetchLimit) { data, error in
  214. guard let data = data, error == nil else {
  215. self.loading = false
  216. completion(error)
  217. return
  218. }
  219. data.forEach { status in
  220. _ = MastodonDataManager.upsertStatus(status.merging(["pinned": true]) { (_, new) in new })
  221. }
  222. }
  223. CoreDataManager.shared.saveContext()
  224. self.loading = false
  225. completion(nil)
  226. }
  227. switch statusTypeSegmentedControl.selectedSegmentIndex {
  228. case 1:
  229. MastodonAPI.statuses(
  230. accountID: account.id!,
  231. onlyMedia: false,
  232. excludeReplies: false,
  233. limit: fetchLimit,
  234. pagination: pagination,
  235. completion: requestCompletion
  236. )
  237. case 2:
  238. MastodonAPI.statuses(
  239. accountID: account.id!,
  240. onlyMedia: true,
  241. excludeReplies: true,
  242. limit: fetchLimit,
  243. pagination: pagination,
  244. completion: requestCompletion
  245. )
  246. default:
  247. MastodonAPI.statuses(
  248. accountID: account.id!,
  249. onlyMedia: false,
  250. excludeReplies: true,
  251. limit: fetchLimit,
  252. pagination: pagination,
  253. completion: requestCompletion
  254. )
  255. }
  256. }
  257. }
  258. }
  259. extension AccountTableViewController: NSFetchedResultsControllerDelegate {
  260. func initializeFetchedResultsController() {
  261. if let account = self.account {
  262. let request = NSFetchRequest<StatusMO>(entityName: "Status")
  263. request.sortDescriptors = [
  264. NSSortDescriptor(key: "pinned", ascending: false),
  265. NSSortDescriptor(key: "createdAt", ascending: false),
  266. ]
  267. switch statusTypeSegmentedControl.selectedSegmentIndex {
  268. case 1:
  269. request.predicate = NSPredicate(format: "account == %@", account)
  270. case 2:
  271. request.predicate = NSPredicate(format: "account == %@ AND attachments.@count > 0", account)
  272. default:
  273. request.predicate = NSPredicate(format: "account == %@ AND inReplyToID == nil", account)
  274. }
  275. fetchedResultsController = NSFetchedResultsController(
  276. fetchRequest: request,
  277. managedObjectContext: CoreDataManager.shared.context,
  278. sectionNameKeyPath: nil,
  279. cacheName: nil
  280. )
  281. fetchedResultsController!.delegate = self
  282. do {
  283. try fetchedResultsController!.performFetch()
  284. } catch {
  285. print("\(error)")
  286. }
  287. }
  288. }
  289. func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  290. tableView.beginUpdates()
  291. }
  292. func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
  293. switch type {
  294. case NSFetchedResultsChangeType.insert:
  295. tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.none)
  296. case NSFetchedResultsChangeType.delete:
  297. tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.none)
  298. case NSFetchedResultsChangeType.update:
  299. if let cell = tableView.cellForRow(at: indexPath!) as? AccountTableViewCell {
  300. updateCell(cell, withStatusAt: indexPath!)
  301. }
  302. case NSFetchedResultsChangeType.move:
  303. tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade)
  304. tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade)
  305. }
  306. }
  307. func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  308. tableView.endUpdates()
  309. }
  310. override func numberOfSections(in tableView: UITableView) -> Int {
  311. return 1
  312. }
  313. // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  314. // if let cell = cell as? AccountTableViewCell {
  315. // CellHeightManager.set(status: cell.statusView.status, height: cell.frame.size.height)
  316. // }
  317. // }
  318. // override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  319. // return CellHeightManager.get(status: fetchedResultsController?.object(at: indexPath))
  320. // }
  321. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  322. return fetchedResultsController?.fetchedObjects?.count ?? 0
  323. }
  324. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  325. guard let cell = tableView.dequeueReusableCell(withIdentifier: "AccountTableViewCell", for: indexPath) as? AccountTableViewCell else {
  326. fatalError("Unable to find reusable cell")
  327. }
  328. updateCell(cell, withStatusAt: indexPath)
  329. cell.statusView.delegate = self
  330. return cell
  331. }
  332. }