A WIP iOS app for Mastodon and Pleroma.
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.

ConversationMainStatusTableViewCell.swift 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. //
  2. // ConversationMainStatusTableViewCell.swift
  3. // Tusker
  4. //
  5. // Created by Shadowfacts on 8/28/18.
  6. // Copyright © 2018 Shadowfacts. All rights reserved.
  7. //
  8. import UIKit
  9. import Pachyderm
  10. class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive {
  11. var delegate: StatusTableViewCellDelegate?
  12. @IBOutlet weak var displayNameLabel: UILabel!
  13. @IBOutlet weak var usernameLabel: UILabel!
  14. @IBOutlet weak var contentLabel: StatusContentLabel!
  15. @IBOutlet weak var avatarImageView: UIImageView!
  16. @IBOutlet weak var timestampLabel: UILabel!
  17. @IBOutlet weak var attachmentsView: UIView!
  18. @IBOutlet weak var favoriteButton: UIButton!
  19. @IBOutlet weak var reblogButton: UIButton!
  20. var statusID: String!
  21. var accountID: String!
  22. var favorited: Bool = false {
  23. didSet {
  24. favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
  25. }
  26. }
  27. var reblogged: Bool = false {
  28. didSet {
  29. reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
  30. }
  31. }
  32. var avatarURL: URL?
  33. var updateTimestampWorkItem: DispatchWorkItem?
  34. override func awakeFromNib() {
  35. displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
  36. displayNameLabel.isUserInteractionEnabled = true
  37. usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
  38. usernameLabel.isUserInteractionEnabled = true
  39. avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
  40. avatarImageView.isUserInteractionEnabled = true
  41. avatarImageView.layer.masksToBounds = true
  42. attachmentsView.layer.cornerRadius = 5
  43. attachmentsView.layer.masksToBounds = true
  44. contentLabel.navigationDelegate = self
  45. }
  46. func updateUIForPreferences() {
  47. guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
  48. avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
  49. displayNameLabel.text = account.realDisplayName
  50. }
  51. func updateUI(for statusID: String) {
  52. self.statusID = statusID
  53. guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
  54. let account: Account
  55. if let reblog = status.reblog {
  56. account = reblog.account
  57. } else {
  58. account = status.account
  59. }
  60. self.accountID = account.id
  61. updateUIForPreferences()
  62. usernameLabel.text = "@\(account.acct)"
  63. avatarImageView.image = nil
  64. avatarURL = account.avatar
  65. ImageCache.avatars.get(account.avatar) { (image) in
  66. DispatchQueue.main.async {
  67. self.avatarImageView.image = image
  68. self.avatarURL = nil
  69. }
  70. }
  71. updateTimestamp()
  72. attachmentsView.subviews.forEach { $0.removeFromSuperview() }
  73. let attachments = status.attachments.filter({ $0.kind == .image })
  74. if attachments.count > 0 {
  75. attachmentsView.isHidden = false
  76. let width = attachmentsView.bounds.width
  77. let height: CGFloat = 200
  78. switch attachments.count {
  79. case 1:
  80. addAttachmentView(frame: CGRect(x: 0, y: 0, width: width, height: height), attachment: attachments[0])
  81. case 2:
  82. addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0])
  83. addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height), attachment: attachments[1])
  84. case 3:
  85. addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0])
  86. addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1])
  87. addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2])
  88. case 4:
  89. addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[0])
  90. addAttachmentView(frame: CGRect(x: 0, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1])
  91. addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2])
  92. addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[3])
  93. default:
  94. fatalError("Too many attachments")
  95. }
  96. } else {
  97. attachmentsView.isHidden = true
  98. }
  99. let realStatus = status.reblog ?? status
  100. favorited = realStatus.favourited ?? false
  101. reblogged = realStatus.reblogged ?? false
  102. contentLabel.statusID = statusID
  103. }
  104. func updateTimestamp() {
  105. guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
  106. timestampLabel.text = status.createdAt.timeAgoString()
  107. let delay: DispatchTimeInterval?
  108. switch status.createdAt.timeAgo().1 {
  109. case .second:
  110. delay = .seconds(10)
  111. case .minute:
  112. delay = .seconds(60)
  113. default:
  114. delay = nil
  115. }
  116. if let delay = delay {
  117. updateTimestampWorkItem = DispatchWorkItem {
  118. self.updateTimestamp()
  119. }
  120. DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
  121. } else {
  122. updateTimestampWorkItem = nil
  123. }
  124. }
  125. func addAttachmentView(frame: CGRect, attachment: Attachment) {
  126. let attachmentView = AttachmentView(frame: frame, attachment: attachment)
  127. attachmentView.delegate = self
  128. attachmentsView.addSubview(attachmentView)
  129. }
  130. override func prepareForReuse() {
  131. if let url = avatarURL {
  132. ImageCache.avatars.cancel(url)
  133. }
  134. updateTimestampWorkItem?.cancel()
  135. updateTimestampWorkItem = nil
  136. attachmentsView.subviews.forEach { view in
  137. (view as? AttachmentView)?.task?.cancel()
  138. }
  139. }
  140. @objc func accountPressed() {
  141. delegate?.selected(account: accountID)
  142. }
  143. @IBAction func replyPressed(_ sender: Any) {
  144. delegate?.reply(to: statusID)
  145. }
  146. @IBAction func favoritePressed(_ sender: Any) {
  147. guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
  148. favorited = !favorited
  149. let realStatus: Status = status.reblog ?? status
  150. let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus)
  151. MastodonController.shared.client.run(request) { response in
  152. self.favorited = realStatus.favourited ?? false
  153. DispatchQueue.main.async {
  154. if case .success = response {
  155. UIImpactFeedbackGenerator(style: .light).impactOccurred()
  156. } else {
  157. print("Couldn't favorite status \(realStatus.id)")
  158. // todo: display error message
  159. UINotificationFeedbackGenerator().notificationOccurred(.error)
  160. return
  161. }
  162. }
  163. }
  164. }
  165. @IBAction func reblogPressed(_ sender: Any) {
  166. guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
  167. reblogged = !reblogged
  168. let realStatus: Status = status.reblog ?? status
  169. let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus)
  170. MastodonController.shared.client.run(request) { response in
  171. self.reblogged = realStatus.reblogged ?? false
  172. DispatchQueue.main.async {
  173. if case .success = response {
  174. UIImpactFeedbackGenerator(style: .light).impactOccurred()
  175. } else {
  176. print("Couldn't reblog status \(realStatus.id)")
  177. // todo: display error message
  178. UINotificationFeedbackGenerator().notificationOccurred(.error)
  179. }
  180. }
  181. }
  182. }
  183. @IBAction func morePressed(_ sender: Any) {
  184. delegate?.showMoreOptions(forStatus: statusID)
  185. }
  186. }
  187. extension ConversationMainStatusTableViewCell: ContentLabelNavigationDelegate {
  188. func selected(mention: Mention) {
  189. delegate?.selected(mention: mention)
  190. }
  191. func selected(tag: Hashtag) {
  192. delegate?.selected(tag: tag)
  193. }
  194. func selected(url: URL) {
  195. delegate?.selected(url: url)
  196. }
  197. }
  198. extension ConversationMainStatusTableViewCell: AttachmentViewDelegate {
  199. func showLargeAttachment(for attachmentView: AttachmentView) {
  200. delegate?.showLargeImage(attachmentView.image!, description: attachmentView.attachment.description, animatingFrom: attachmentView)
  201. }
  202. }