// // StatusPollView.swift // Tusker // // Created by Shadowfacts on 4/25/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class StatusPollView: UIView { private static let formatter: DateComponentsFormatter = { let f = DateComponentsFormatter() f.includesTimeRemainingPhrase = true f.maximumUnitCount = 1 f.unitsStyle = .full f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] return f }() weak var mastodonController: MastodonController! weak var toastableViewController: ToastableViewController? private var statusID: String! private(set) var poll: Poll? private var optionsView: PollOptionsView! private var voteButton: PollVoteButton! 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 = PollVoteButton() voteButton.translatesAutoresizingMaskIntoConstraints = false voteButton.addTarget(self, action: #selector(votePressed)) voteButton.setFont(infoLabel.font) 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), infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor), infoLabel.trailingAnchor.constraint(lessThanOrEqualTo: voteButton.leadingAnchor, constant: -8), voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44), voteButton.trailingAnchor.constraint(equalTo: trailingAnchor), voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor), voteButton.bottomAnchor.constraint(equalTo: bottomAnchor), ]) accessibilityElements = [optionsView!, infoLabel!, voteButton!] } func updateUI(status: StatusMO, poll: Poll?) { 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.isEnabled = canVote optionsView.updateUI(poll: poll) var expired = false let expiryText: String? if let expiresAt = poll.expiresAt { if expiresAt > Date() { expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt) } else { expired = true expiryText = nil } } else { expiryText = "Does not expire" } 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)" } if expired { voteButton.disabledTitle = "Expired" } else if poll.voted ?? false { if status.account.id == mastodonController.account?.id { voteButton.isHidden = true } else { voteButton.disabledTitle = "Voted" } } else if poll.multiple { voteButton.disabledTitle = "Select multiple" } else { voteButton.disabledTitle = "Select one" } voteButton.isEnabled = false } private func checkedOptionsChanged() { voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0 } @objc private func votePressed() { optionsView.isEnabled = false voteButton.isEnabled = false voteButton.disabledTitle = "Voted" UIImpactFeedbackGenerator(style: .light).impactOccurred() 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: self.statusID)!, poll: self.poll) if let toastable = self.toastableViewController { let config = ToastConfiguration(from: error, with: "Error Voting", in: toastable, retryAction: nil) toastable.showToast(configuration: config, animated: true) } } case let .success(poll, _): let container = self.mastodonController.persistentContainer DispatchQueue.main.async { guard let status = container.status(for: self.statusID) else { return } status.poll = poll container.save(context: container.viewContext) container.statusSubject.send(status.id) } } } } }