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

403 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
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
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
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
  1. //
  2. // StatusStackView.swift
  3. // elpha-ios
  4. //
  5. // Created by Dwayne Harris on 10/9/18.
  6. // Copyright © 2018 Elpha. All rights reserved.
  7. //
  8. import Kingfisher
  9. import UIKit
  10. protocol StatusViewDelegate: class {
  11. func accountTapped(_ sender: Any, account: AccountMO)
  12. func statusTapped(_ sender: Any, status: StatusMO)
  13. func loadMoreTapped(_ sender: Any, status: StatusMO, direction: PaginationDirection)
  14. func attachmentTapped(_ sender: Any, status: StatusMO, index: Int)
  15. func urlTapped(_ sender: Any, url: URL)
  16. func boostTapped(_ sender: Any)
  17. func favoriteTapped(_ sender: Any)
  18. func replyTapped(_ sender: Any, status: StatusMO)
  19. func revealTapped(_ sender: Any)
  20. func hideTapped(_ sender: Any)
  21. }
  22. class StatusView: UIView {
  23. @IBOutlet var contentView: UIView!
  24. @IBOutlet var boostView: UIView!
  25. @IBOutlet var boostAvatarImageView: UIImageView!
  26. @IBOutlet var boostDisplayNameLabel: UILabel!
  27. @IBOutlet var boostUsernameLabel: UILabel!
  28. @IBOutlet var replyView: UIView!
  29. @IBOutlet var replyAvatarImageView: UIImageView!
  30. @IBOutlet var replyDisplayNameLabel: UILabel!
  31. @IBOutlet var replyUsernameLabel: UILabel!
  32. @IBOutlet var avatarImageView: UIImageView!
  33. @IBOutlet var displayNameLabel: UILabel!
  34. @IBOutlet var usernameLabel: UILabel!
  35. @IBOutlet var timestampLabel: UILabel!
  36. @IBOutlet var repliesImageView: UIImageView!
  37. @IBOutlet var repliesLabel: UILabel!
  38. @IBOutlet var boostsImageView: UIImageView!
  39. @IBOutlet var boostsLabel: UILabel!
  40. @IBOutlet var favoritesImageView: UIImageView!
  41. @IBOutlet var favoritesLabel: UILabel!
  42. @IBOutlet var topDividerView: UIView!
  43. @IBOutlet var bottomDividerView: UIView!
  44. @IBOutlet var attachmentsView: AttachmentsView!
  45. @IBOutlet var detailsView: UIView!
  46. @IBOutlet var spoilerImageView: UIImageView!
  47. @IBOutlet var pinImageView: UIImageView!
  48. @IBOutlet var topLoadMoreView: UIView!
  49. @IBOutlet var bottomLoadMoreView: UIView!
  50. @IBOutlet var contentTextView: UITextView!
  51. var status: StatusMO?
  52. var spoilerView: UIVisualEffectView?
  53. var feedbackGenerator: UINotificationFeedbackGenerator?
  54. weak var delegate: StatusViewDelegate?
  55. @IBAction func boostViewTapped(_ sender: Any) {
  56. if let delegate = delegate, let status = status {
  57. delegate.accountTapped(self, account: status.account!)
  58. }
  59. }
  60. @IBAction func replyViewTapped(_ sender: Any) {
  61. if let delegate = delegate,
  62. let status = status,
  63. let replyAccountID = status.inReplyToAccountID,
  64. let replyAccount = MastodonDataManager.account(id: replyAccountID) {
  65. delegate.accountTapped(self, account: replyAccount)
  66. }
  67. }
  68. @IBAction func accountViewTapped(_ sender: Any) {
  69. if let delegate = delegate, let status = status {
  70. if let reblog = status.reblog {
  71. delegate.accountTapped(self, account: reblog.account!)
  72. } else {
  73. delegate.accountTapped(self, account: status.account!)
  74. }
  75. }
  76. }
  77. @IBAction func mainViewTapped(_ sender: Any) {
  78. if let delegate = delegate, let status = status {
  79. if let reblog = status.reblog {
  80. delegate.statusTapped(self, status: reblog)
  81. } else {
  82. delegate.statusTapped(self, status: status)
  83. }
  84. }
  85. }
  86. @IBAction func spoilerImageTapped(_ sender: Any) {
  87. if let status = status {
  88. status.hidden = true
  89. CoreDataManager.shared.saveContext()
  90. delegate?.hideTapped(self)
  91. }
  92. }
  93. @IBAction func topLoadMoreViewTapped(_ sender: Any) {
  94. if let delegate = delegate, let status = status {
  95. delegate.loadMoreTapped(self, status: status, direction: .prev)
  96. }
  97. }
  98. @IBAction func bottomLoadMoreViewTapped(_ sender: Any) {
  99. if let delegate = delegate, let status = status {
  100. delegate.loadMoreTapped(self, status: status, direction: .next)
  101. }
  102. }
  103. @IBAction func boostTapped(_ sender: Any) {
  104. if let status = status {
  105. let allowedVisibility = [StatusVisibility.public, StatusVisibility.unlisted]
  106. let visibility = StatusVisibility(rawValue: status.visibility!)
  107. guard allowedVisibility.contains(visibility!) else {
  108. return
  109. }
  110. feedbackGenerator?.prepare()
  111. if status.reblogged {
  112. status.reblogged = false
  113. status.reblogsCount = status.reblogsCount - 1
  114. MastodonAPI.unreblog(statusID: status.id!) { error in
  115. guard error == nil else {
  116. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  117. self.feedbackGenerator?.notificationOccurred(.error)
  118. return
  119. }
  120. AlertManager.shared.show(message: "Unboosted", category: .boosted)
  121. self.feedbackGenerator?.notificationOccurred(.success)
  122. }
  123. } else {
  124. status.reblogged = true
  125. status.reblogsCount = status.reblogsCount + 1
  126. MastodonAPI.reblog(statusID: status.id!) { error in
  127. guard error == nil else {
  128. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  129. self.feedbackGenerator?.notificationOccurred(.error)
  130. return
  131. }
  132. AlertManager.shared.show(message: "Boosted!", category: .boosted)
  133. self.feedbackGenerator?.notificationOccurred(.success)
  134. }
  135. }
  136. CoreDataManager.shared.saveContext()
  137. delegate?.boostTapped(self)
  138. }
  139. }
  140. @IBAction func favoriteTapped(_ sender: Any) {
  141. if let status = status {
  142. feedbackGenerator?.prepare()
  143. if status.favorited {
  144. status.favorited = false
  145. status.favoritesCount = status.favoritesCount - 1
  146. MastodonAPI.unfavorite(statusID: status.id!) { error in
  147. guard error == nil else {
  148. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  149. self.feedbackGenerator?.notificationOccurred(.error)
  150. return
  151. }
  152. AlertManager.shared.show(message: "Unfavorited", category: .favorited)
  153. self.feedbackGenerator?.notificationOccurred(.success)
  154. }
  155. } else {
  156. status.favorited = true
  157. status.favoritesCount = status.favoritesCount + 1
  158. MastodonAPI.favorite(statusID: status.id!) { error in
  159. guard error == nil else {
  160. AlertManager.shared.show(message: error!.localizedDescription, category: .error)
  161. self.feedbackGenerator?.notificationOccurred(.error)
  162. return
  163. }
  164. AlertManager.shared.show(message: "Favorited!", category: .favorited)
  165. self.feedbackGenerator?.notificationOccurred(.success)
  166. }
  167. }
  168. CoreDataManager.shared.saveContext()
  169. delegate?.favoriteTapped(self)
  170. }
  171. }
  172. @IBAction func replyTapped(_ sender: Any) {
  173. delegate?.replyTapped(self, status: status!)
  174. }
  175. @objc func reveal(sender: UITapGestureRecognizer) {
  176. if let status = status {
  177. status.hidden = false
  178. CoreDataManager.shared.saveContext()
  179. delegate?.revealTapped(self)
  180. }
  181. }
  182. override init(frame: CGRect) {
  183. super.init(frame: frame)
  184. setup()
  185. }
  186. required init?(coder aDecoder: NSCoder) {
  187. super.init(coder: aDecoder)
  188. setup()
  189. }
  190. private func setup() {
  191. Bundle.main.loadNibNamed("StatusView", owner: self, options: nil)
  192. addSubview(contentView)
  193. contentView.frame = self.bounds
  194. attachmentsView.delegate = self
  195. contentTextView.delegate = self
  196. feedbackGenerator = UINotificationFeedbackGenerator()
  197. avatarImageView.setRoundedCorners()
  198. boostAvatarImageView.setRoundedCorners()
  199. replyAvatarImageView.setRoundedCorners()
  200. }
  201. public func update(withStatus status: StatusMO, excludeReplyView: Bool = false) {
  202. self.status = status
  203. let statusCellHidden = status.hidden
  204. spoilerImageView.isHidden = true
  205. let avatarOptions: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame]
  206. func updateStatusContent(_ status: StatusMO) {
  207. if let account = status.account {
  208. avatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions)
  209. displayNameLabel.text = account.displayName
  210. usernameLabel.text = "@\(account.acct!)"
  211. }
  212. if let attachments = status.attachments, attachments.count > 0 {
  213. UIView.performWithoutAnimation {
  214. attachmentsView.isHidden = false
  215. attachmentsView.update(withAttachments: attachments.array as! [AttachmentMO])
  216. }
  217. } else {
  218. attachmentsView.isHidden = true
  219. }
  220. contentTextView.attributedText = status.attributedContent
  221. if statusCellHidden {
  222. if spoilerView == nil {
  223. let spoilerText = UILabel(frame: contentTextView.frame)
  224. spoilerText.textColor = UIColor(named: "Primary")
  225. spoilerText.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  226. spoilerText.textAlignment = .center
  227. spoilerText.text = status.spoilerText
  228. spoilerText.numberOfLines = 0
  229. spoilerText.translatesAutoresizingMaskIntoConstraints = false
  230. let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffect.Style.light))
  231. blurEffectView.translatesAutoresizingMaskIntoConstraints = false
  232. blurEffectView.contentView.addSubview(spoilerText)
  233. NSLayoutConstraint.activate([
  234. spoilerText.leadingAnchor.constraint(equalTo: blurEffectView.leadingAnchor, constant: 8),
  235. spoilerText.trailingAnchor.constraint(equalTo: blurEffectView.trailingAnchor, constant: -8),
  236. spoilerText.centerYAnchor.constraint(equalTo: blurEffectView.centerYAnchor),
  237. ])
  238. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.reveal))
  239. tapGesture.numberOfTapsRequired = 1
  240. tapGesture.numberOfTouchesRequired = 1
  241. blurEffectView.addGestureRecognizer(tapGesture)
  242. blurEffectView.isUserInteractionEnabled = true
  243. self.spoilerView = blurEffectView
  244. self.contentView.addSubview(blurEffectView)
  245. NSLayoutConstraint.activate([
  246. blurEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
  247. blurEffectView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
  248. blurEffectView.topAnchor.constraint(equalTo: contentTextView.topAnchor),
  249. blurEffectView.bottomAnchor.constraint(equalTo: detailsView.topAnchor),
  250. ])
  251. }
  252. } else {
  253. if let spoilerView = spoilerView {
  254. spoilerView.removeFromSuperview()
  255. self.spoilerView = nil
  256. }
  257. if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
  258. spoilerImageView.isHidden = false
  259. }
  260. }
  261. timestampLabel.text = status.createdAt!.timeAgo()
  262. repliesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.repliesCount), number: .decimal)
  263. boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal)
  264. favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favoritesCount), number: .decimal)
  265. let visibility = StatusVisibility(rawValue: status.visibility!)
  266. switch visibility! {
  267. case .private:
  268. boostsImageView.image = UIImage(named: "Private")
  269. case .direct:
  270. boostsImageView.image = UIImage(named: "Direct")
  271. default:
  272. if status.reblogged {
  273. boostsImageView.image = UIImage(named: "Boost Bold")
  274. } else {
  275. boostsImageView.image = UIImage(named: "Boost Regular")
  276. }
  277. }
  278. if status.favorited {
  279. favoritesImageView.image = UIImage(named: "Star Filled")
  280. } else {
  281. favoritesImageView.image = UIImage(named: "Star Regular")
  282. }
  283. pinImageView.isHidden = !status.pinned
  284. }
  285. if let reblog = status.reblog {
  286. boostView.isHidden = false
  287. if let account = status.account {
  288. boostAvatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions)
  289. boostDisplayNameLabel.text = account.displayName
  290. boostUsernameLabel.text = "@\(account.acct!)"
  291. }
  292. updateStatusContent(reblog)
  293. } else {
  294. boostView.isHidden = true
  295. if let account = status.account {
  296. avatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions)
  297. displayNameLabel.text = account.displayName
  298. usernameLabel.text = "@\(account.acct!)"
  299. }
  300. updateStatusContent(status)
  301. }
  302. if let replyAccountID = status.inReplyToAccountID, let replyAccount = MastodonDataManager.account(id: replyAccountID), !excludeReplyView {
  303. replyView.isHidden = false
  304. replyAvatarImageView.kf.setImage(with: replyAccount.avatarURL!, options: avatarOptions)
  305. replyDisplayNameLabel.text = replyAccount.displayName
  306. replyUsernameLabel.text = "@\(replyAccount.acct!)"
  307. } else {
  308. replyView.isHidden = true
  309. }
  310. }
  311. }
  312. extension StatusView: AttachmentsViewDelegate {
  313. func attachmentTapped(_ sender: Any, index: Int) {
  314. if let delegate = delegate, let status = status {
  315. if let reblog = status.reblog {
  316. delegate.attachmentTapped(self, status: reblog, index: index)
  317. } else {
  318. delegate.attachmentTapped(self, status: status, index: index)
  319. }
  320. }
  321. }
  322. }
  323. extension StatusView: UITextViewDelegate {
  324. func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
  325. if let mentions = status?.mentions {
  326. for mention in mentions {
  327. if URL == mention.url {
  328. if let account = MastodonDataManager.account(id: mention.id) {
  329. delegate?.accountTapped(self, account: account)
  330. }
  331. return false
  332. }
  333. }
  334. }
  335. if URL.scheme == "http" || URL.scheme == "https" {
  336. delegate?.urlTapped(self, url: URL)
  337. }
  338. return false
  339. }
  340. }