metatext-app-ios-iphone-ipad/Views/Status/StatusView.swift

362 lines
16 KiB
Swift

// Copyright © 2020 Metabolist. All rights reserved.
import Kingfisher
import UIKit
class StatusView: UIView {
@IBOutlet var baseView: UIView!
@IBOutlet weak var metaIcon: UIImageView!
@IBOutlet weak var metaLabel: UILabel!
@IBOutlet weak var contentTextView: TouchFallthroughTextView!
@IBOutlet weak var avatarButton: UIButton!
@IBOutlet weak var avatarImageView: AnimatedImageView!
@IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var accountLabel: UILabel!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var spoilerTextLabel: UILabel!
@IBOutlet weak var toggleSensitiveContentButton: UIButton!
@IBOutlet weak var replyButton: UIButton!
@IBOutlet weak var reblogButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var shareButton: UIButton!
@IBOutlet weak var attachmentsView: AttachmentsView!
@IBOutlet weak var cardView: UIView!
@IBOutlet weak var cardImageView: UIImageView!
@IBOutlet weak var cardTitleLabel: UILabel!
@IBOutlet weak var cardDescriptionLabel: UILabel!
@IBOutlet weak var cardURLLabel: UILabel!
@IBOutlet weak var cardButton: UIButton!
@IBOutlet weak var sensitiveContentView: UIStackView!
@IBOutlet weak var hasReplyFollowingView: UIView!
@IBOutlet weak var inReplyToView: UIView!
@IBOutlet weak var avatarReplyContextView: UIView!
@IBOutlet weak var nameDateView: UIStackView!
@IBOutlet weak var contextParentAvatarNameView: UIStackView!
@IBOutlet weak var contextParentAvatarImageView: AnimatedImageView!
@IBOutlet weak var contextParentAvatarButton: UIButton!
@IBOutlet weak var contextParentDisplayNameLabel: UILabel!
@IBOutlet weak var contextParentAccountLabel: UILabel!
@IBOutlet weak var actionButtonsView: UIStackView!
@IBOutlet weak var contextParentReplyButton: UIButton!
@IBOutlet weak var contextParentReblogButton: UIButton!
@IBOutlet weak var contextParentFavoriteButton: UIButton!
@IBOutlet weak var contextParentShareButton: UIButton!
@IBOutlet weak var contextParentActionsButton: UIButton!
@IBOutlet weak var contextParentTimeLabel: UILabel!
@IBOutlet weak var timeApplicationDividerView: UILabel!
@IBOutlet weak var applicationButton: UIButton!
@IBOutlet weak var contextParentRebloggedByButton: UIButton!
@IBOutlet weak var contextParentFavoritedByButton: UIButton!
@IBOutlet weak var contextParentItems: UIStackView!
@IBOutlet weak var contextParentRebloggedByFavoritedByView: UIStackView!
@IBOutlet weak var contextParentRebloggedByFavoritedBySeparator: UIView!
private var statusConfiguration: StatusContentConfiguration
@IBOutlet private var separatorConstraints: [NSLayoutConstraint]!
init(configuration: StatusContentConfiguration) {
self.statusConfiguration = configuration
super.init(frame: .zero)
initialSetup()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
for button: UIButton in [toggleSensitiveContentButton] where button.frame.height != 0 {
button.layer.cornerRadius = button.frame.height / 2
}
}
}
extension StatusView: UIContentView {
var configuration: UIContentConfiguration {
get { statusConfiguration }
set {
guard let statusConfiguration = newValue as? StatusContentConfiguration else { return }
self.statusConfiguration = statusConfiguration
avatarImageView.kf.cancelDownloadTask()
contextParentAvatarImageView.kf.cancelDownloadTask()
cardImageView.kf.cancelDownloadTask()
applyStatusConfiguration()
}
}
}
extension StatusView: UITextViewDelegate {
func textView(
_ textView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool {
switch interaction {
case .invokeDefaultAction:
statusConfiguration.viewModel.urlSelected(URL)
return false
case .preview: return false
case .presentActions: return false
@unknown default: return false
}
}
}
private extension StatusView {
func initialSetup() {
Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)
addSubview(baseView)
baseView.translatesAutoresizingMaskIntoConstraints = false
baseView.backgroundColor = .clear
NSLayoutConstraint.activate([
baseView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
baseView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
baseView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
baseView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
// These have "Placeholder" checked in the xib file so they can
// be set to go beyond the readable content guide
inReplyToView.topAnchor.constraint(equalTo: topAnchor),
hasReplyFollowingView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
for constraint in separatorConstraints {
constraint.constant = 1 / UIScreen.main.scale
}
avatarImageView.kf.indicatorType = .activity
contextParentAvatarImageView.kf.indicatorType = .activity
cardImageView.kf.indicatorType = .activity
contentTextView.delegate = self
let highlightedButtonBackgroundImage = UIColor(white: 0, alpha: 0.5).image()
cardButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
avatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
contextParentAvatarButton.setBackgroundImage(highlightedButtonBackgroundImage, for: .highlighted)
let accountAction = UIAction { [weak self] _ in self?.statusConfiguration.viewModel.accountSelected() }
avatarButton.addAction(accountAction, for: .touchUpInside)
contextParentAvatarButton.addAction(accountAction, for: .touchUpInside)
let favoriteAction = UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleFavorited() }
favoriteButton.addAction(favoriteAction, for: .touchUpInside)
contextParentFavoriteButton.addAction(favoriteAction, for: .touchUpInside)
shareButton.addAction(
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.shareStatus() },
for: .touchUpInside)
contextParentFavoritedByButton.addAction(
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.favoritedBySelected() },
for: .touchUpInside)
applyStatusConfiguration()
}
// swiftlint:disable function_body_length
func applyStatusConfiguration() {
let viewModel = statusConfiguration.viewModel
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
let contentFont = UIFont.preferredFont(forTextStyle: viewModel.isContextParent ? .title3 : .callout)
contentTextView.shouldFallthrough = !viewModel.isContextParent
avatarReplyContextView.isHidden = viewModel.isContextParent
nameDateView.isHidden = viewModel.isContextParent
contextParentAvatarNameView.isHidden = !viewModel.isContextParent
actionButtonsView.isHidden = viewModel.isContextParent
contextParentItems.isHidden = !viewModel.isContextParent
let avatarImageView: UIImageView
let displayNameLabel: UILabel
let accountLabel: UILabel
if viewModel.isContextParent {
avatarImageView = contextParentAvatarImageView
displayNameLabel = contextParentDisplayNameLabel
accountLabel = contextParentAccountLabel
} else {
avatarImageView = self.avatarImageView
displayNameLabel = self.displayNameLabel
accountLabel = self.accountLabel
}
let contentRange = NSRange(location: 0, length: mutableContent.length)
mutableContent.removeAttribute(.font, range: contentRange)
mutableContent.addAttributes(
[.font: contentFont as Any,
.foregroundColor: UIColor.label],
range: contentRange)
mutableContent.insert(emoji: viewModel.contentEmoji, view: contentTextView)
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
contentTextView.attributedText = mutableContent
contentTextView.isHidden = contentTextView.text == ""
mutableDisplayName.insert(emoji: viewModel.displayNameEmoji, view: displayNameLabel)
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
displayNameLabel.attributedText = mutableDisplayName
mutableSpoilerText.insert(emoji: viewModel.contentEmoji, view: spoilerTextLabel)
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
spoilerTextLabel.attributedText = mutableSpoilerText
spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == ""
toggleSensitiveContentButton.setTitle(
viewModel.shouldDisplaySensitiveContent
? NSLocalizedString("status.show-less", comment: "")
: NSLocalizedString("status.show-more", comment: ""),
for: .normal)
accountLabel.text = viewModel.accountName
timeLabel.text = viewModel.time
contextParentTimeLabel.text = viewModel.contextParentTime
timeApplicationDividerView.isHidden = viewModel.applicationName == nil
applicationButton.isHidden = viewModel.applicationName == nil
applicationButton.setTitle(viewModel.applicationName, for: .normal)
applicationButton.isEnabled = viewModel.applicationURL != nil
avatarImageView.kf.setImage(with: viewModel.avatarURL)
toggleSensitiveContentButton.isHidden = !viewModel.sensitive
replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal)
reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal)
setReblogButtonColor(reblogged: viewModel.reblogged)
favoriteButton.setTitle(viewModel.favoritesCount == 0 ? "" : String(viewModel.favoritesCount), for: .normal)
setFavoriteButtonColor(favorited: viewModel.favorited)
reblogButton.isEnabled = viewModel.canBeReblogged
contextParentReblogButton.isEnabled = viewModel.canBeReblogged
let noReblogs = viewModel.reblogsCount == 0
let noFavorites = viewModel.favoritesCount == 0
let noInteractions = noReblogs && noFavorites
setAttributedLocalizedTitle(
button: contextParentRebloggedByButton,
localizationKey: "status.reblogs-count",
count: viewModel.reblogsCount)
contextParentRebloggedByButton.isHidden = noReblogs
setAttributedLocalizedTitle(
button: contextParentFavoritedByButton,
localizationKey: "status.favorites-count",
count: viewModel.favoritesCount)
contextParentFavoritedByButton.isHidden = noFavorites
contextParentRebloggedByFavoritedByView.isHidden = noInteractions
contextParentRebloggedByFavoritedBySeparator.isHidden = noInteractions
if
viewModel.isReblog {
let metaText = String.localizedStringWithFormat(
NSLocalizedString("status.reblogged-by", comment: ""),
viewModel.rebloggedByDisplayName)
let mutableMetaText = NSMutableAttributedString(string: metaText)
mutableMetaText.insert(emoji: viewModel.rebloggedByDisplayNameEmoji, view: metaLabel)
mutableMetaText.resizeAttachments(toLineHeight: metaLabel.font.lineHeight)
metaLabel.attributedText = mutableMetaText
metaIcon.image = UIImage(
systemName: "arrow.2.squarepath",
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
metaLabel.isHidden = false
metaIcon.isHidden = false
} else if viewModel.isPinned {
metaLabel.text = NSLocalizedString("status.pinned-post", comment: "")
metaIcon.image = UIImage(
systemName: "pin",
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
metaLabel.isHidden = false
metaIcon.isHidden = false
} else {
metaLabel.isHidden = true
metaIcon.isHidden = true
}
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
attachmentsView.attachmentViewModels = viewModel.attachmentViewModels
setNeedsLayout()
if let cardURL = viewModel.cardURL {
cardTitleLabel.text = viewModel.cardTitle
cardDescriptionLabel.text = viewModel.cardDescription
cardDescriptionLabel.isHidden = cardDescriptionLabel.text == ""
|| cardDescriptionLabel.text == cardTitleLabel.text
if
let host = cardURL.host, host.hasPrefix("www."),
let withoutWww = cardURL.host?.components(separatedBy: "www.").last {
cardURLLabel.text = withoutWww
} else {
cardURLLabel.text = cardURL.host
}
if let cardImageURL = viewModel.cardImageURL {
cardImageView.isHidden = false
cardImageView.kf.setImage(with: cardImageURL)
} else {
cardImageView.isHidden = true
}
cardView.isHidden = false
} else {
cardView.isHidden = true
}
sensitiveContentView.isHidden = !viewModel.shouldDisplaySensitiveContent
inReplyToView.isHidden = !viewModel.isReplyInContext
hasReplyFollowingView.isHidden = !viewModel.hasReplyFollowing
}
// swiftlint:enable function_body_length
func setReblogButtonColor(reblogged: Bool) {
let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel
let reblogButton: UIButton
if statusConfiguration.viewModel.isContextParent {
reblogButton = contextParentReblogButton
} else {
reblogButton = self.reblogButton
}
reblogButton.tintColor = reblogColor
reblogButton.setTitleColor(reblogColor, for: .normal)
}
func setFavoriteButtonColor(favorited: Bool) {
let favoriteColor: UIColor = favorited ? .systemYellow : .secondaryLabel
let favoriteButton: UIButton
let scale: UIImage.SymbolScale
if statusConfiguration.viewModel.isContextParent {
favoriteButton = contextParentFavoriteButton
scale = .medium
} else {
favoriteButton = self.favoriteButton
scale = .small
}
favoriteButton.tintColor = favoriteColor
favoriteButton.setTitleColor(favoriteColor, for: .normal)
favoriteButton.setImage(UIImage(
systemName: favorited ? "star.fill" : "star",
withConfiguration: UIImage.SymbolConfiguration(scale: scale)),
for: .normal)
}
private func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) {
let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count)
button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal)
button.setAttributedTitle(
localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true),
for: .highlighted)
}
}