Tusker/Tusker/Views/Poll/StatusPollView.swift

195 lines
6.9 KiB
Swift

//
// 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 = .full
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 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)
voteButton.setContentCompressionResistancePriority(.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),
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: 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.mastodonController = mastodonController
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
}
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
guard poll != nil else { return 0 }
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
}
private func checkedOptionsChanged() {
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
}
@objc private func votePressed() {
guard let statusID,
let poll else {
return
}
optionsView.isEnabled = false
voteButton.isEnabled = false
voteButton.disabledTitle = "Voted"
#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)
}
}
}
}
}