// // StatusPollView.swift // Tusker // // Created by Shadowfacts on 4/25/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class StatusPollView: UIView, StatusContentView { private static let formatter: DateComponentsFormatter = { let f = DateComponentsFormatter() f.includesTimeRemainingPhrase = true f.maximumUnitCount = 1 f.unitsStyle = .abbreviated f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] return f }() weak var mastodonController: MastodonController! weak var delegate: TuskerNavigationDelegate? private var statusID: String! private(set) var poll: Poll? private var showingResults = false private var optionsView: PollOptionsView! private var voteButton: UIButton! private var infoLabel: UILabel! private var canVote = true private var animator: UIViewPropertyAnimator! private var currentSelectedOptionIndex: Int! var isTracking: Bool { optionsView.isTracking } override init(frame: CGRect) { super.init(frame: .zero) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { backgroundColor = .clear optionsView = PollOptionsView(frame: .zero) optionsView.translatesAutoresizingMaskIntoConstraints = false optionsView.checkedOptionsChanged = self.checkedOptionsChanged addSubview(optionsView) infoLabel = UILabel() infoLabel.translatesAutoresizingMaskIntoConstraints = false infoLabel.textColor = .secondaryLabel infoLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout), size: 0) infoLabel.adjustsFontSizeToFitWidth = true addSubview(infoLabel) voteButton = UIButton(configuration: .plain()) voteButton.translatesAutoresizingMaskIntoConstraints = false voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside) voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) voteButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) addSubview(voteButton) NSLayoutConstraint.activate([ optionsView.leadingAnchor.constraint(equalTo: leadingAnchor), optionsView.trailingAnchor.constraint(equalTo: trailingAnchor), optionsView.topAnchor.constraint(equalTo: topAnchor), infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor), infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor, constant: 4), infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor), infoLabel.trailingAnchor.constraint(equalTo: voteButton.leadingAnchor, constant: -8), voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44), voteButton.trailingAnchor.constraint(equalTo: trailingAnchor), voteButton.topAnchor.constraint(equalTo: infoLabel.topAnchor), voteButton.bottomAnchor.constraint(equalTo: infoLabel.bottomAnchor), ]) accessibilityElements = [optionsView!, infoLabel!, voteButton!] } func updateUI(status: StatusMO, poll: Poll?) { if statusID != status.id { showingResults = false } self.statusID = status.id self.poll = poll guard let poll = poll else { return } // poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account?.id { canVote = false } else { canVote = true } optionsView.mastodonController = mastodonController optionsView.isEnabled = canVote optionsView.updateUI(poll: poll, showResults: showingResults || !canVote) let expiryText: String? if let expiresAt = poll.expiresAt { if expiresAt > Date() { expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt) } else { expiryText = nil } } else { expiryText = "No expiry" } let format = NSLocalizedString("poll votes count", comment: "poll total votes count") infoLabel.text = String.localizedStringWithFormat(format, poll.votesCount) if let expiryText = expiryText { infoLabel.text! += ", \(expiryText)" } updateVoteButton(status: status, poll: poll) } func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { guard poll != nil else { return 0 } return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height } private func updateVoteButton(status: StatusMO, poll: Poll) { let buttonTitle: String let buttonEnabled: Bool if let expiresAt = poll.expiresAt, expiresAt <= Date() { buttonTitle = "Expired" buttonEnabled = false } else if poll.voted ?? false { if status.account.id == mastodonController.account?.id { voteButton.isHidden = true return } else { buttonTitle = "Voted" buttonEnabled = false } } else if optionsView.checkedOptionIndices.isEmpty { buttonTitle = "See Results" buttonEnabled = true } else { buttonTitle = "Vote" buttonEnabled = true } var config = UIButton.Configuration.plain() config.attributedTitle = AttributedString(buttonTitle) config.attributedTitle!.font = infoLabel.font config.contentInsets = .zero // Necessary on Catalyst for some reason. config.baseForegroundColor = .tintColor voteButton.configuration = config voteButton.isEnabled = buttonEnabled } private func checkedOptionsChanged() { if let status = mastodonController.persistentContainer.status(for: statusID), let poll = status.poll { updateVoteButton(status: status, poll: poll) } } @objc private func votePressed() { if !optionsView.checkedOptionIndices.isEmpty { doVote() } else { showResults() } } private func doVote() { guard let statusID, let poll else { return } optionsView.isEnabled = false voteButton.isEnabled = false #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() #endif let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices) mastodonController.run(request) { (response) in switch response { case let .failure(error): DispatchQueue.main.async { self.updateUI(status: self.mastodonController.persistentContainer.status(for: statusID)!, poll: poll) if let delegate = self.delegate { let config = ToastConfiguration(from: error, with: "Error Voting", in: delegate, retryAction: nil) delegate.showToast(configuration: config, animated: true) } } case let .success(poll, _): let container = self.mastodonController.persistentContainer DispatchQueue.main.async { guard let status = container.status(for: statusID) else { return } status.poll = poll container.save(context: container.viewContext) container.statusSubject.send(status.id) } } } } private func showResults() { showingResults = true if let status = mastodonController.persistentContainer.status(for: statusID) { updateUI(status: status, poll: status.poll) } } }