// // StatusStackView.swift // elpha-ios // // Created by Dwayne Harris on 10/9/18. // Copyright © 2018 Elpha. All rights reserved. // import Kingfisher import UIKit protocol StatusViewDelegate: class { func accountTapped(_ sender: Any, account: AccountMO) func statusTapped(_ sender: Any, status: StatusMO) func loadMoreTapped(_ sender: Any, status: StatusMO, direction: PaginationDirection) func attachmentTapped(_ sender: Any, status: StatusMO, index: Int) func urlTapped(_ sender: Any, url: URL) func boostTapped(_ sender: Any) func favoriteTapped(_ sender: Any) func replyTapped(_ sender: Any, status: StatusMO) func revealTapped(_ sender: Any) func hideTapped(_ sender: Any) } class StatusView: UIView { @IBOutlet var contentView: UIView! @IBOutlet var boostView: UIView! @IBOutlet var boostAvatarImageView: UIImageView! @IBOutlet var boostDisplayNameLabel: UILabel! @IBOutlet var boostUsernameLabel: UILabel! @IBOutlet var replyView: UIView! @IBOutlet var replyAvatarImageView: UIImageView! @IBOutlet var replyDisplayNameLabel: UILabel! @IBOutlet var replyUsernameLabel: UILabel! @IBOutlet var avatarImageView: UIImageView! @IBOutlet var displayNameLabel: UILabel! @IBOutlet var usernameLabel: UILabel! @IBOutlet var timestampLabel: UILabel! @IBOutlet var repliesImageView: UIImageView! @IBOutlet var repliesLabel: UILabel! @IBOutlet var boostsImageView: UIImageView! @IBOutlet var boostsLabel: UILabel! @IBOutlet var favoritesImageView: UIImageView! @IBOutlet var favoritesLabel: UILabel! @IBOutlet var topDividerView: UIView! @IBOutlet var bottomDividerView: UIView! @IBOutlet var attachmentsView: AttachmentsView! @IBOutlet var detailsView: UIView! @IBOutlet var spoilerImageView: UIImageView! @IBOutlet var pinImageView: UIImageView! @IBOutlet var topLoadMoreView: UIView! @IBOutlet var bottomLoadMoreView: UIView! @IBOutlet var contentTextView: UITextView! var status: StatusMO? var spoilerView: UIVisualEffectView? var feedbackGenerator: UINotificationFeedbackGenerator? weak var delegate: StatusViewDelegate? @IBAction func boostViewTapped(_ sender: Any) { if let delegate = delegate, let status = status { delegate.accountTapped(self, account: status.account!) } } @IBAction func replyViewTapped(_ sender: Any) { if let delegate = delegate, let status = status, let replyAccountID = status.inReplyToAccountID, let replyAccount = MastodonDataManager.account(id: replyAccountID) { delegate.accountTapped(self, account: replyAccount) } } @IBAction func accountViewTapped(_ sender: Any) { if let delegate = delegate, let status = status { if let reblog = status.reblog { delegate.accountTapped(self, account: reblog.account!) } else { delegate.accountTapped(self, account: status.account!) } } } @IBAction func mainViewTapped(_ sender: Any) { if let delegate = delegate, let status = status { if let reblog = status.reblog { delegate.statusTapped(self, status: reblog) } else { delegate.statusTapped(self, status: status) } } } @IBAction func spoilerImageTapped(_ sender: Any) { if let status = status { status.hidden = true CoreDataManager.shared.saveContext() delegate?.hideTapped(self) } } @IBAction func topLoadMoreViewTapped(_ sender: Any) { if let delegate = delegate, let status = status { delegate.loadMoreTapped(self, status: status, direction: .prev) } } @IBAction func bottomLoadMoreViewTapped(_ sender: Any) { if let delegate = delegate, let status = status { delegate.loadMoreTapped(self, status: status, direction: .next) } } @IBAction func boostTapped(_ sender: Any) { if let status = status { let allowedVisibility = [StatusVisibility.public, StatusVisibility.unlisted] let visibility = StatusVisibility(rawValue: status.visibility!) guard allowedVisibility.contains(visibility!) else { return } 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() delegate?.boostTapped(self) } } @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() delegate?.favoriteTapped(self) } } @IBAction func replyTapped(_ sender: Any) { delegate?.replyTapped(self, status: status!) } @objc func reveal(sender: UITapGestureRecognizer) { if let status = status { status.hidden = false CoreDataManager.shared.saveContext() delegate?.revealTapped(self) } } override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } private func setup() { Bundle.main.loadNibNamed("StatusView", owner: self, options: nil) addSubview(contentView) contentView.frame = self.bounds attachmentsView.delegate = self contentTextView.delegate = self feedbackGenerator = UINotificationFeedbackGenerator() avatarImageView.setRoundedCorners() boostAvatarImageView.setRoundedCorners() replyAvatarImageView.setRoundedCorners() } public func update(withStatus status: StatusMO, excludeReplyView: Bool = false) { self.status = status let statusCellHidden = status.hidden spoilerImageView.isHidden = true let avatarOptions: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame] func updateStatusContent(_ status: StatusMO) { if let account = status.account { avatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions) displayNameLabel.text = account.displayName usernameLabel.text = "@\(account.acct!)" } if let attachments = status.attachments, attachments.count > 0 { UIView.performWithoutAnimation { attachmentsView.isHidden = false attachmentsView.update(withAttachments: attachments.array as! [AttachmentMO]) } } else { attachmentsView.isHidden = true } contentTextView.attributedText = status.attributedContent if statusCellHidden { if spoilerView == nil { let spoilerText = UILabel(frame: contentTextView.frame) spoilerText.textColor = UIColor(named: "Primary") spoilerText.font = UIFont.systemFont(ofSize: 15, weight: .bold) spoilerText.textAlignment = .center spoilerText.text = status.spoilerText spoilerText.numberOfLines = 0 spoilerText.translatesAutoresizingMaskIntoConstraints = false let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffect.Style.light)) blurEffectView.translatesAutoresizingMaskIntoConstraints = false blurEffectView.contentView.addSubview(spoilerText) NSLayoutConstraint.activate([ spoilerText.leadingAnchor.constraint(equalTo: blurEffectView.leadingAnchor, constant: 8), spoilerText.trailingAnchor.constraint(equalTo: blurEffectView.trailingAnchor, constant: -8), spoilerText.centerYAnchor.constraint(equalTo: blurEffectView.centerYAnchor), ]) let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.reveal)) tapGesture.numberOfTapsRequired = 1 tapGesture.numberOfTouchesRequired = 1 blurEffectView.addGestureRecognizer(tapGesture) blurEffectView.isUserInteractionEnabled = true self.spoilerView = blurEffectView self.contentView.addSubview(blurEffectView) NSLayoutConstraint.activate([ blurEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), blurEffectView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), blurEffectView.topAnchor.constraint(equalTo: contentTextView.topAnchor), blurEffectView.bottomAnchor.constraint(equalTo: detailsView.topAnchor), ]) } } else { if let spoilerView = spoilerView { spoilerView.removeFromSuperview() self.spoilerView = nil } if let spoilerText = status.spoilerText, !spoilerText.isEmpty { spoilerImageView.isHidden = false } } timestampLabel.text = status.createdAt!.timeAgo() repliesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.repliesCount), number: .decimal) boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal) favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favoritesCount), number: .decimal) let visibility = StatusVisibility(rawValue: status.visibility!) switch visibility! { case .private: boostsImageView.image = UIImage(named: "Private") case .direct: boostsImageView.image = UIImage(named: "Direct") default: if status.reblogged { boostsImageView.image = UIImage(named: "Boost Bold") } else { boostsImageView.image = UIImage(named: "Boost Regular") } } if status.favorited { favoritesImageView.image = UIImage(named: "Star Filled") } else { favoritesImageView.image = UIImage(named: "Star Regular") } pinImageView.isHidden = !status.pinned } if let reblog = status.reblog { boostView.isHidden = false if let account = status.account { boostAvatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions) boostDisplayNameLabel.text = account.displayName boostUsernameLabel.text = "@\(account.acct!)" } updateStatusContent(reblog) } else { boostView.isHidden = true if let account = status.account { avatarImageView.kf.setImage(with: account.avatarURL!, options: avatarOptions) displayNameLabel.text = account.displayName usernameLabel.text = "@\(account.acct!)" } updateStatusContent(status) } if let replyAccountID = status.inReplyToAccountID, let replyAccount = MastodonDataManager.account(id: replyAccountID), !excludeReplyView { replyView.isHidden = false replyAvatarImageView.kf.setImage(with: replyAccount.avatarURL!, options: avatarOptions) replyDisplayNameLabel.text = replyAccount.displayName replyUsernameLabel.text = "@\(replyAccount.acct!)" } else { replyView.isHidden = true } } } extension StatusView: AttachmentsViewDelegate { func attachmentTapped(_ sender: Any, index: Int) { if let delegate = delegate, let status = status { if let reblog = status.reblog { delegate.attachmentTapped(self, status: reblog, index: index) } else { delegate.attachmentTapped(self, status: status, index: index) } } } } extension StatusView: 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) { delegate?.accountTapped(self, account: account) } return false } } } if URL.scheme == "http" || URL.scheme == "https" { delegate?.urlTapped(self, url: URL) } return false } }