[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

//
// 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
}
}