// // StatusEditCollectionViewCell.swift // Tusker // // Created by Shadowfacts on 5/11/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm @MainActor protocol StatusEditCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate { func statusEditCellNeedsReconfigure(_ cell: StatusEditCollectionViewCell, animated: Bool, completion: (() -> Void)?) } class StatusEditCollectionViewCell: UICollectionViewListCell { private lazy var contentVStack = UIStackView(arrangedSubviews: [ timestampLabel, contentWarningLabel, collapseButton, contentContainer, ]).configure { $0.axis = .vertical $0.spacing = 4 $0.alignment = .fill } private lazy var timestampLabel = UILabel().configure { $0.textColor = .secondaryLabel $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ .traits: [ UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, ] ]), size: 0) $0.adjustsFontForContentSizeCategory = true } private lazy var contentWarningLabel = EmojiLabel().configure { $0.numberOfLines = 0 $0.textColor = .secondaryLabel $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ .traits: [ UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue ] ]), size: 0) $0.adjustsFontForContentSizeCategory = true $0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.isUserInteractionEnabled = true $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed))) } private lazy var collapseButton = StatusCollapseButton(configuration: { var config = UIButton.Configuration.filled() config.image = UIImage(systemName: "chevron.down") return config }()).configure { $0.tintAdjustmentMode = .normal $0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) } private lazy var contentContainer = StatusContentContainer(arrangedSubviews: [ contentTextView, cardView, attachmentsView, pollView, ] as! [any StatusContentView], useTopSpacer: false).configure { $0.setContentHuggingPriority(.defaultLow, for: .vertical) } private let contentTextView = ContentTextView().configure { $0.adjustsFontForContentSizeCategory = true $0.isScrollEnabled = false $0.backgroundColor = nil $0.isEditable = false $0.isSelectable = false } private let cardView = StatusCardView().configure { $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true } private let attachmentsView = AttachmentsContainerView() private let pollView = StatusEditPollView() weak var delegate: StatusEditCollectionViewCellDelegate? private var mastodonController: MastodonController! { delegate?.apiController } private var edit: StatusEdit! private var statusState: CollapseState! override init(frame: CGRect) { super.init(frame: frame) contentVStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(contentVStack) NSLayoutConstraint.activate([ contentVStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), contentVStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6), contentVStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), contentVStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: Accessibility override var isAccessibilityElement: Bool { get { true } set {} } override var accessibilityAttributedLabel: NSAttributedString? { get { var str: AttributedString = "" if statusState.collapsed ?? false { if !edit.spoilerText.isEmpty { str += AttributedString(edit.spoilerText) str += ", " } str += "collapsed" } else { str += AttributedString(contentTextView.attributedText) if edit.attachments.count > 0 { let includeDescriptions: Bool switch Preferences.shared.attachmentBlurMode { case .useStatusSetting: includeDescriptions = !Preferences.shared.blurMediaBehindContentWarning || edit.spoilerText.isEmpty case .always: includeDescriptions = true case .never: includeDescriptions = false } if includeDescriptions { if edit.attachments.count == 1 { let attachment = edit.attachments[0] let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description" str += AttributedString(", attachment: \(desc)") } else { for (index, attachment) in edit.attachments.enumerated() { let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description" str += AttributedString(", attachment \(index + 1): \(desc)") } } } else { str += AttributedString(", \(edit.attachments.count) attachment\(edit.attachments.count == 1 ? "" : "s")") } } if edit.poll != nil { str += ", poll" } } return NSAttributedString(str) } set {} } override var accessibilityHint: String? { get { if statusState.collapsed ?? false { return "Double tap to expand the post." } else { return nil } } set {} } override func accessibilityActivate() -> Bool { if statusState.collapsed ?? false { collapseButtonPressed() } return true } // MARK: Configure UI func updateUI(edit: StatusEdit, state: CollapseState, index: Int) { self.edit = edit self.statusState = state timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) contentTextView.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(edit.content) contentTextView.setEmojis(edit.emojis, identifier: index) contentTextView.navigationDelegate = delegate attachmentsView.delegate = self attachmentsView.updateUI(attachments: edit.attachments) pollView.isHidden = edit.poll == nil pollView.updateUI(poll: edit.poll, emojis: edit.emojis) cardView.isHidden = true contentWarningLabel.text = edit.spoilerText contentWarningLabel.isHidden = edit.spoilerText.isEmpty if !contentWarningLabel.isHidden { contentWarningLabel.setEmojis(edit.emojis, identifier: index) } _ = state.resolveFor(status: edit, height: { let width = self.bounds.width - 2*16 return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width) }) collapseButton.isHidden = !state.collapsible! contentContainer.setCollapsed(state.collapsed!) if state.collapsed! { contentContainer.alpha = 0 // TODO: is this accessing the image view before the button's been laid out? collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: 0) collapseButton.accessibilityLabel = NSLocalizedString("Expand Status", comment: "expand status button accessibility label") } else { contentContainer.alpha = 1 collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: .pi) collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label") } } // MARK: Interaction @objc private func collapseButtonPressed() { statusState.collapsed!.toggle() contentContainer.layer.masksToBounds = true delegate?.statusEditCellNeedsReconfigure(self, animated: true) { self.contentContainer.layer.masksToBounds = false } } } extension StatusEditCollectionViewCell: AttachmentViewDelegate { func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? { guard let delegate else { return nil } let attachments = attachmentsView.attachments! let sourceViews = attachments.map { attachmentsView.getAttachmentView(for: $0) } let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index) return gallery } func attachmentViewPresent(_ vc: UIViewController, animated: Bool) { delegate?.present(vc, animated: animated) } }