2021-04-28 23:00:17 +00:00
|
|
|
//
|
|
|
|
// StatusPollView.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 4/25/21.
|
|
|
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import Pachyderm
|
|
|
|
|
2023-12-04 22:06:10 +00:00
|
|
|
class StatusPollView: UIView, StatusContentView {
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
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!
|
2023-05-10 14:34:48 +00:00
|
|
|
weak var delegate: TuskerNavigationDelegate?
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
private var statusID: String!
|
2021-05-22 15:30:40 +00:00
|
|
|
private(set) var poll: Poll?
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
private var optionsView: PollOptionsView!
|
2022-05-01 16:22:15 +00:00
|
|
|
private var voteButton: PollVoteButton!
|
2021-04-28 23:00:17 +00:00
|
|
|
private var infoLabel: UILabel!
|
|
|
|
|
|
|
|
private var canVote = true
|
|
|
|
private var animator: UIViewPropertyAnimator!
|
|
|
|
private var currentSelectedOptionIndex: Int!
|
|
|
|
|
2021-06-12 23:22:51 +00:00
|
|
|
var isTracking: Bool {
|
|
|
|
optionsView.isTracking
|
|
|
|
}
|
|
|
|
|
2022-10-08 19:12:17 +00:00
|
|
|
override init(frame: CGRect) {
|
|
|
|
super.init(frame: .zero)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
super.init(coder: coder)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func commonInit() {
|
2021-04-28 23:00:17 +00:00
|
|
|
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)
|
|
|
|
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton = PollVoteButton()
|
2021-04-28 23:00:17 +00:00
|
|
|
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.addTarget(self, action: #selector(votePressed))
|
|
|
|
voteButton.setFont(infoLabel.font)
|
2023-05-27 22:11:53 +00:00
|
|
|
voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
2021-04-28 23:00:17 +00:00
|
|
|
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),
|
2023-05-27 22:11:53 +00:00
|
|
|
infoLabel.trailingAnchor.constraint(equalTo: voteButton.leadingAnchor, constant: -8),
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
|
|
|
|
voteButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
|
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
|
|
|
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
])
|
2021-06-07 01:50:45 +00:00
|
|
|
|
|
|
|
accessibilityElements = [optionsView!, infoLabel!, voteButton!]
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
2021-05-22 15:30:40 +00:00
|
|
|
func updateUI(status: StatusMO, poll: Poll?) {
|
2021-04-28 23:00:17 +00:00
|
|
|
self.statusID = status.id
|
|
|
|
self.poll = poll
|
|
|
|
|
2021-05-22 15:30:40 +00:00
|
|
|
guard let poll = poll else { return }
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
|
2022-11-15 02:31:30 +00:00
|
|
|
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account?.id {
|
2021-04-28 23:00:17 +00:00
|
|
|
canVote = false
|
|
|
|
} else {
|
|
|
|
canVote = true
|
|
|
|
}
|
|
|
|
|
2023-02-19 23:21:20 +00:00
|
|
|
optionsView.mastodonController = mastodonController
|
2021-04-28 23:00:17 +00:00
|
|
|
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 {
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.disabledTitle = "Expired"
|
2021-04-28 23:00:17 +00:00
|
|
|
} else if poll.voted ?? false {
|
2022-11-15 02:31:30 +00:00
|
|
|
if status.account.id == mastodonController.account?.id {
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.isHidden = true
|
2021-05-04 03:18:15 +00:00
|
|
|
} else {
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.disabledTitle = "Voted"
|
2021-05-04 03:18:15 +00:00
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
} else if poll.multiple {
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.disabledTitle = "Select multiple"
|
2021-04-28 23:00:17 +00:00
|
|
|
} else {
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.disabledTitle = "Select one"
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
voteButton.isEnabled = false
|
|
|
|
}
|
|
|
|
|
2023-05-13 19:00:03 +00:00
|
|
|
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat {
|
2023-05-27 22:11:53 +00:00
|
|
|
guard poll != nil else { return 0 }
|
2023-05-13 19:00:03 +00:00
|
|
|
return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height
|
|
|
|
}
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
private func checkedOptionsChanged() {
|
|
|
|
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func votePressed() {
|
2023-05-11 18:57:47 +00:00
|
|
|
guard let statusID,
|
|
|
|
let poll else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
optionsView.isEnabled = false
|
|
|
|
voteButton.isEnabled = false
|
2022-05-01 16:22:15 +00:00
|
|
|
voteButton.disabledTitle = "Voted"
|
2021-04-28 23:00:17 +00:00
|
|
|
|
2023-01-27 02:28:56 +00:00
|
|
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
2021-05-05 21:46:41 +00:00
|
|
|
|
2023-05-11 18:57:47 +00:00
|
|
|
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices)
|
2021-04-28 23:00:17 +00:00
|
|
|
mastodonController.run(request) { (response) in
|
|
|
|
switch response {
|
|
|
|
case let .failure(error):
|
2022-05-13 14:00:11 +00:00
|
|
|
DispatchQueue.main.async {
|
2023-05-11 18:57:47 +00:00
|
|
|
self.updateUI(status: self.mastodonController.persistentContainer.status(for: statusID)!, poll: poll)
|
2022-05-13 14:00:11 +00:00
|
|
|
|
2023-05-10 14:34:48 +00:00
|
|
|
if let delegate = self.delegate {
|
|
|
|
let config = ToastConfiguration(from: error, with: "Error Voting", in: delegate, retryAction: nil)
|
|
|
|
delegate.showToast(configuration: config, animated: true)
|
2022-05-13 14:00:11 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
case let .success(poll, _):
|
|
|
|
let container = self.mastodonController.persistentContainer
|
|
|
|
DispatchQueue.main.async {
|
2023-05-11 18:57:47 +00:00
|
|
|
guard let status = container.status(for: statusID) else {
|
2021-04-28 23:00:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
status.poll = poll
|
2022-10-09 21:06:10 +00:00
|
|
|
container.save(context: container.viewContext)
|
2021-04-28 23:00:17 +00:00
|
|
|
container.statusSubject.send(status.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|