metatext/Views/UIKit/Content Views/AnnouncementView.swift
2022-11-07 22:15:05 -08:00

180 lines
6.8 KiB
Swift

// Copyright © 2021 Metabolist. All rights reserved.
import Mastodon
import UIKit
import ViewModels
final class AnnouncementView: UIView {
private let contentTextView = TouchFallthroughTextView()
private let reactionButton = UIButton()
private let reactionsCollectionView = AnnouncementReactionsCollectionView()
private var announcementConfiguration: AnnouncementContentConfiguration
init(configuration: AnnouncementContentConfiguration) {
announcementConfiguration = configuration
super.init(frame: .zero)
initialSetup()
applyAnnouncementConfiguration()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var dataSource: UICollectionViewDiffableDataSource<Int, AnnouncementReaction> = {
let cellRegistration = UICollectionView.CellRegistration
<AnnouncementReactionCollectionViewCell, AnnouncementReaction> { [weak self] in
guard let self = self else { return }
$0.viewModel = AnnouncementReactionViewModel(
announcementReaction: $2,
identityContext: self.announcementConfiguration.viewModel.identityContext)
}
let dataSource = UICollectionViewDiffableDataSource
<Int, AnnouncementReaction>(collectionView: reactionsCollectionView) {
$0.dequeueConfiguredReusableCell(using: cellRegistration, for: $1, item: $2)
}
return dataSource
}()
}
extension AnnouncementView {
static func estimatedHeight(width: CGFloat, announcement: Announcement) -> CGFloat {
UITableView.automaticDimension
}
func dismissIfUnread() {
announcementConfiguration.viewModel.dismissIfUnread()
}
}
extension AnnouncementView: UIContentView {
var configuration: UIContentConfiguration {
get { announcementConfiguration }
set {
guard let announcementConfiguration = newValue as? AnnouncementContentConfiguration else { return }
self.announcementConfiguration = announcementConfiguration
applyAnnouncementConfiguration()
}
}
}
extension AnnouncementView: UITextViewDelegate {
func textView(
_ textView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool {
switch interaction {
case .invokeDefaultAction:
announcementConfiguration.viewModel.urlSelected(URL)
return false
case .preview: return false
case .presentActions: return false
@unknown default: return false
}
}
}
extension AnnouncementView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
guard let reaction = dataSource.itemIdentifier(for: indexPath) else { return }
if reaction.me {
announcementConfiguration.viewModel.removeReaction(name: reaction.name)
} else {
announcementConfiguration.viewModel.addReaction(name: reaction.name)
}
UISelectionFeedbackGenerator().selectionChanged()
}
}
private extension AnnouncementView {
func initialSetup() {
let stackView = UIStackView()
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = .defaultSpacing
contentTextView.adjustsFontForContentSizeCategory = true
contentTextView.backgroundColor = .clear
contentTextView.delegate = self
stackView.addArrangedSubview(contentTextView)
let reactionStackView = UIStackView()
stackView.addArrangedSubview(reactionStackView)
reactionStackView.spacing = .defaultSpacing
reactionStackView.alignment = .top
reactionStackView.addArrangedSubview(reactionButton)
reactionButton.tag = UUID().hashValue
reactionButton.accessibilityLabel = NSLocalizedString("announcement.insert-emoji", comment: "")
reactionButton.setImage(
UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
for: .normal)
reactionButton.addAction(
UIAction { [weak self] _ in
guard let self = self else { return }
self.announcementConfiguration.viewModel.presentEmojiPicker(sourceViewTag: self.reactionButton.tag)
},
for: .touchUpInside)
reactionStackView.addArrangedSubview(reactionsCollectionView)
reactionsCollectionView.delegate = self
reactionsCollectionView.setContentCompressionResistancePriority(.required, for: .vertical)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
reactionButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension),
reactionButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
])
}
func applyAnnouncementConfiguration() {
let viewModel = announcementConfiguration.viewModel
let mutableContent = NSMutableAttributedString(attributedString: viewModel.announcement.content.attributed)
let contentFont = UIFont.preferredFont(forTextStyle: .callout)
let contentRange = NSRange(location: 0, length: mutableContent.length)
mutableContent.removeAttribute(.font, range: contentRange)
mutableContent.addAttributes(
[.font: contentFont, .foregroundColor: UIColor.label],
range: contentRange)
mutableContent.insert(emojis: viewModel.announcement.emojis,
view: contentTextView,
identityContext: viewModel.identityContext)
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
contentTextView.attributedText = mutableContent
var snapshot = NSDiffableDataSourceSnapshot<Int, AnnouncementReaction>()
snapshot.appendSections([0])
snapshot.appendItems(viewModel.announcement.reactions, toSection: 0)
if snapshot.itemIdentifiers != dataSource.snapshot().itemIdentifiers {
dataSource.apply(snapshot, animatingDifferences: false) {
if self.contentTextView.frame.size == .zero
|| self.contentTextView.contentSize.height < self.contentTextView.frame.height {
viewModel.reload()
}
}
}
}
}