// Copyright © 2020 Metabolist. All rights reserved. import Combine import Mastodon import UIKit import ViewModels final class PollView: UIView { private let stackView = UIStackView() private let bottomStackView = UIStackView() private let voteButton = CapsuleButton() private let refreshButton = UIButton(type: .system) private let refreshDividerLabel = UILabel() private let votesCountLabel = UILabel() private let votesCountDividerLabel = UILabel() private let expiryLabel = UILabel() private var selectionCancellable: AnyCancellable? var viewModel: StatusViewModel? { didSet { for view in stackView.arrangedSubviews { stackView.removeArrangedSubview(view) view.removeFromSuperview() } guard let viewModel = viewModel else { selectionCancellable = nil return } let accessibilityAttributedLabel = NSMutableAttributedString( string: NSLocalizedString("status.poll.accessibility-label", comment: "")) if !viewModel.isPollExpired, !viewModel.hasVotedInPoll { for (index, option) in viewModel.pollOptions.enumerated() { let button = PollOptionButton( title: option.title, emojis: viewModel.pollEmojis, multipleSelection: viewModel.isPollMultipleSelection, identityContext: viewModel.identityContext) button.button.addAction( UIAction { _ in if viewModel.pollOptionSelections.contains(index) { viewModel.pollOptionSelections.remove(index) } else if viewModel.isPollMultipleSelection { viewModel.pollOptionSelections.insert(index) } else { viewModel.pollOptionSelections = [index] } }, for: .touchUpInside) stackView.addArrangedSubview(button) } } else { for (index, option) in viewModel.pollOptions.enumerated() { let resultView = PollResultView( option: option, emojis: viewModel.pollEmojis, selected: viewModel.pollOwnVotes.contains(index), multipleSelection: viewModel.isPollMultipleSelection, votersCount: viewModel.pollVotersCount, identityContext: viewModel.identityContext) stackView.addArrangedSubview(resultView) } } for (index, view) in stackView.arrangedSubviews.enumerated() { var title: NSAttributedString? var percent: String? let indexLabel = String.localizedStringWithFormat( NSLocalizedString("status.poll.option-%ld", comment: ""), index + 1) if let optionView = view as? PollOptionButton, let attributedTitle = optionView.button.accessibilityAttributedLabel { title = attributedTitle let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel) if viewModel.isPollMultipleSelection { optionAccessibilityAttributedLabel.appendWithSeparator( NSLocalizedString("compose.poll.accessibility.multiple-choices-allowed", comment: "")) } optionAccessibilityAttributedLabel.appendWithSeparator(attributedTitle) optionView.accessibilityAttributedLabel = optionAccessibilityAttributedLabel } else if let resultView = view as? PollResultView { title = resultView.titleLabel.attributedText percent = resultView.percentLabel.text } guard let presentTitle = title else { continue } accessibilityAttributedLabel.appendWithSeparator(indexLabel) accessibilityAttributedLabel.appendWithSeparator(presentTitle) if let percent = percent { accessibilityAttributedLabel.appendWithSeparator(percent) } } if !viewModel.isPollExpired, !viewModel.hasVotedInPoll { stackView.addArrangedSubview(voteButton) selectionCancellable = viewModel.$pollOptionSelections.sink { [weak self] in guard let self = self else { return } for (index, view) in self.stackView.arrangedSubviews.enumerated() { (view as? PollOptionButton)?.isSelected = $0.contains(index) } self.voteButton.isEnabled = !$0.isEmpty } } else { selectionCancellable = nil } stackView.addArrangedSubview(bottomStackView) let votesCount = String.localizedStringWithFormat( NSLocalizedString("status.poll.participation-count-%ld", comment: ""), viewModel.pollVotersCount) votesCountLabel.text = votesCount votesCountLabel.isAccessibilityElement = true votesCountLabel.accessibilityLabel = votesCountLabel.text accessibilityAttributedLabel.appendWithSeparator(votesCount) if !viewModel.isPollExpired, let pollTimeLeft = viewModel.pollTimeLeft { expiryLabel.text = String.localizedStringWithFormat( NSLocalizedString("status.poll.time-left-%@", comment: ""), pollTimeLeft) refreshButton.isHidden = false accessibilityCustomActions = [UIAccessibilityCustomAction( name: NSLocalizedString("status.poll.refresh", comment: "")) { [weak self] _ in self?.viewModel?.refreshPoll() return true }] } else { expiryLabel.text = NSLocalizedString("status.poll.closed", comment: "") refreshButton.isHidden = true accessibilityCustomActions = nil } expiryLabel.isAccessibilityElement = true expiryLabel.accessibilityLabel = expiryLabel.text if let expiry = expiryLabel.text { accessibilityAttributedLabel.appendWithSeparator(expiry) } refreshDividerLabel.isHidden = refreshButton.isHidden self.accessibilityAttributedLabel = accessibilityAttributedLabel } } override init(frame: CGRect) { super.init(frame: frame) initialSetup() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension PollView { static func estimatedHeight(width: CGFloat, identityContext: IdentityContext, status: Status, configuration: CollectionItem.StatusConfiguration) -> CGFloat { if let poll = status.displayStatus.poll { var height: CGFloat = 0 let open = !poll.expired && !poll.voted for option in poll.options { if open { height += PollOptionButton.estimatedHeight(width: width, title: option.title) } else { height += PollResultView.estimatedHeight(width: width, title: option.title) } height += .defaultSpacing } if open { height += .minimumButtonDimension + .defaultSpacing } height += .minimumButtonDimension / 2 return height } else { return 0 } } } private extension PollView { func initialSetup() { addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = .defaultSpacing voteButton.setTitle(NSLocalizedString("status.poll.vote", comment: ""), for: .normal) voteButton.addAction(UIAction { [weak self] _ in self?.viewModel?.vote() }, for: .touchUpInside) bottomStackView.spacing = .compactSpacing bottomStackView.addArrangedSubview(refreshButton) refreshButton.titleLabel?.font = .preferredFont(forTextStyle: .caption1) refreshButton.titleLabel?.adjustsFontForContentSizeCategory = true refreshButton.setTitle(NSLocalizedString("status.poll.refresh", comment: ""), for: .normal) refreshButton.addAction(UIAction { [weak self] _ in self?.viewModel?.refreshPoll() }, for: .touchUpInside) for label in [refreshDividerLabel, votesCountLabel, votesCountDividerLabel, expiryLabel] { bottomStackView.addArrangedSubview(label) label.font = .preferredFont(forTextStyle: .caption1) label.textColor = .secondaryLabel label.adjustsFontForContentSizeCategory = true } refreshDividerLabel.text = "•" votesCountDividerLabel.text = "•" bottomStackView.addArrangedSubview(UIView()) let refreshButtonHeightConstraint = refreshButton.heightAnchor.constraint( equalToConstant: .minimumButtonDimension / 2) refreshButtonHeightConstraint.priority = .justBelowMax NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: leadingAnchor), stackView.topAnchor.constraint(equalTo: topAnchor), stackView.trailingAnchor.constraint(equalTo: trailingAnchor), stackView.bottomAnchor.constraint(equalTo: bottomAnchor), refreshButtonHeightConstraint ]) } }