forked from shadowfacts/Tusker
Shadowfacts
94f71541f8
# Conflicts: # Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift # Tusker/Screens/Timeline/TimelineViewController.swift # Tusker/Views/Status/TimelineStatusCollectionViewCell.swift
195 lines
6.9 KiB
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|