mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Polls
This commit is contained in:
parent
ff2f813280
commit
8ad01a6ecf
12 changed files with 434 additions and 5 deletions
|
@ -201,6 +201,17 @@ public extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func update(id: Status.Id, poll: Poll) -> AnyPublisher<Never, Error> {
|
||||
databaseWriter.writePublisher {
|
||||
let data = try StatusRecord.databaseJSONEncoder(for: StatusRecord.Columns.poll.name).encode(poll)
|
||||
|
||||
try StatusRecord.filter(StatusRecord.Columns.id == id)
|
||||
.updateAll($0, StatusRecord.Columns.poll.set(to: data))
|
||||
}
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||
databaseWriter.writePublisher {
|
||||
try list.save($0)
|
||||
|
|
|
@ -91,7 +91,6 @@
|
|||
"status.show-more" = "Show More";
|
||||
"status.show-less" = "Show Less";
|
||||
"status.poll.vote" = "Vote";
|
||||
"status.poll.participation-count" = "%ld people";
|
||||
"status.poll.time-left" = "%@ left";
|
||||
"status.poll.refresh" = "Refresh";
|
||||
"status.poll.closed" = "Closed";
|
||||
|
|
|
@ -2,6 +2,22 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>status.poll.participation-count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@people@</string>
|
||||
<key>people</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>%ld person</string>
|
||||
<key>other</key>
|
||||
<string>%ld people</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>status.reblogs-count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
|
45
MastodonAPI/Sources/MastodonAPI/Endpoints/PollEndpoint.swift
Normal file
45
MastodonAPI/Sources/MastodonAPI/Endpoints/PollEndpoint.swift
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Mastodon
|
||||
|
||||
public enum PollEndpoint {
|
||||
case poll(id: Poll.Id)
|
||||
case votes(id: Poll.Id, choices: [Int])
|
||||
}
|
||||
|
||||
extension PollEndpoint: Endpoint {
|
||||
public typealias ResultType = Poll
|
||||
|
||||
public var context: [String] {
|
||||
defaultContext + ["polls"]
|
||||
}
|
||||
|
||||
public var pathComponentsInContext: [String] {
|
||||
switch self {
|
||||
case let .poll(id):
|
||||
return [id]
|
||||
case let .votes(id, _):
|
||||
return [id, "votes"]
|
||||
}
|
||||
}
|
||||
|
||||
public var jsonBody: [String: Any]? {
|
||||
switch self {
|
||||
case .poll:
|
||||
return nil
|
||||
case let .votes(_, choices):
|
||||
return ["choices": choices]
|
||||
}
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
switch self {
|
||||
case .poll:
|
||||
return .get
|
||||
case .votes:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,9 @@
|
|||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */; };
|
||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */; };
|
||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D71254246E200B1EBEF /* PollView.swift */; };
|
||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D812544D80000B1EBEF /* PollOptionButton.swift */; };
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */; };
|
||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
||||
|
@ -129,6 +132,9 @@
|
|||
D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomDismissalInteractionController.swift; sourceTree = "<group>"; };
|
||||
D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomTransitionController.swift; sourceTree = "<group>"; };
|
||||
D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomAnimatableView.swift; sourceTree = "<group>"; };
|
||||
D08B8D71254246E200B1EBEF /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = "<group>"; };
|
||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionButton.swift; sourceTree = "<group>"; };
|
||||
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultView.swift; sourceTree = "<group>"; };
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||
|
@ -333,6 +339,9 @@
|
|||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
||||
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */,
|
||||
D08B8D71254246E200B1EBEF /* PollView.swift */,
|
||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
||||
|
@ -607,13 +616,16 @@
|
|||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||
|
|
|
@ -53,4 +53,20 @@ public extension StatusService {
|
|||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func vote(selectedOptions: Set<Int>) -> AnyPublisher<Never, Error> {
|
||||
guard let poll = status.displayStatus.poll else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
return mastodonAPIClient.request(PollEndpoint.votes(id: poll.id, choices: Array(selectedOptions)))
|
||||
.flatMap { contentDatabase.update(id: status.displayStatus.id, poll: $0) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func refreshPoll() -> AnyPublisher<Never, Error> {
|
||||
guard let poll = status.displayStatus.poll else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
return mastodonAPIClient.request(PollEndpoint.poll(id: poll.id))
|
||||
.flatMap { contentDatabase.update(id: status.displayStatus.id, poll: $0) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
|||
|
||||
switch item {
|
||||
case let .status(status, configuration):
|
||||
var viewModel: StatusViewModel
|
||||
let viewModel: StatusViewModel
|
||||
|
||||
if let cachedViewModel = cachedViewModel as? StatusViewModel {
|
||||
viewModel = cachedViewModel
|
||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public struct StatusViewModel: CollectionItemViewModel {
|
||||
public final class StatusViewModel: CollectionItemViewModel, ObservableObject {
|
||||
public let content: NSAttributedString
|
||||
public let contentEmoji: [Emoji]
|
||||
public let displayName: String
|
||||
|
@ -15,8 +15,8 @@ public struct StatusViewModel: CollectionItemViewModel {
|
|||
public let rebloggedByDisplayName: String
|
||||
public let rebloggedByDisplayNameEmoji: [Emoji]
|
||||
public let attachmentViewModels: [AttachmentViewModel]
|
||||
public let pollOptionTitles: [String]
|
||||
public let pollEmoji: [Emoji]
|
||||
@Published public var pollOptionSelections = Set<Int>()
|
||||
public var configuration = CollectionItem.StatusConfiguration.default
|
||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||
|
||||
|
@ -41,7 +41,6 @@ public struct StatusViewModel: CollectionItemViewModel {
|
|||
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
||||
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
||||
.map { AttachmentViewModel(attachment: $0, status: statusService.status, identification: identification) }
|
||||
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
||||
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -112,6 +111,30 @@ public extension StatusViewModel {
|
|||
|
||||
var sharingURL: URL? { statusService.status.displayStatus.url }
|
||||
|
||||
var isPollExpired: Bool { statusService.status.displayStatus.poll?.expired ?? true }
|
||||
|
||||
var hasVotedInPoll: Bool { statusService.status.displayStatus.poll?.voted ?? false }
|
||||
|
||||
var isPollMultipleSelection: Bool { statusService.status.displayStatus.poll?.multiple ?? false }
|
||||
|
||||
var pollOptions: [Poll.Option] { statusService.status.displayStatus.poll?.options ?? [] }
|
||||
|
||||
var pollVotersCount: Int {
|
||||
guard let poll = statusService.status.displayStatus.poll else { return 0 }
|
||||
|
||||
return poll.votersCount ?? poll.votesCount
|
||||
}
|
||||
|
||||
var pollOwnVotes: Set<Int> { Set(statusService.status.displayStatus.poll?.ownVotes ?? []) }
|
||||
|
||||
var pollTimeLeft: String? {
|
||||
guard let expiresAt = statusService.status.displayStatus.poll?.expiresAt,
|
||||
expiresAt > Date()
|
||||
else { return nil }
|
||||
|
||||
return expiresAt.fullUnitTimeUntil
|
||||
}
|
||||
|
||||
var cardViewModel: CardViewModel? {
|
||||
if let card = statusService.status.displayStatus.card {
|
||||
return CardViewModel(card: card)
|
||||
|
@ -191,6 +214,20 @@ public extension StatusViewModel {
|
|||
|
||||
eventsSubject.send(Just(.share(url)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func vote() {
|
||||
eventsSubject.send(
|
||||
statusService.vote(selectedOptions: pollOptionSelections)
|
||||
.map { _ in .ignorableOutput }
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func refreshPoll() {
|
||||
eventsSubject.send(
|
||||
statusService.refreshPoll()
|
||||
.map { _ in .ignorableOutput }
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatusViewModel {
|
||||
|
|
46
Views/PollOptionButton.swift
Normal file
46
Views/PollOptionButton.swift
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import UIKit
|
||||
|
||||
class PollOptionButton: UIButton {
|
||||
init(title: String, emoji: [Emoji], multipleSelection: Bool) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
titleLabel?.font = .preferredFont(forTextStyle: .callout)
|
||||
titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
contentHorizontalAlignment = .leading
|
||||
titleEdgeInsets = Self.titleEdgeInsets
|
||||
|
||||
let attributedTitle = NSMutableAttributedString(string: title)
|
||||
|
||||
attributedTitle.insert(emoji: emoji, view: titleLabel!)
|
||||
attributedTitle.resizeAttachments(toLineHeight: titleLabel!.font.lineHeight)
|
||||
setAttributedTitle(attributedTitle, for: .normal)
|
||||
setImage(
|
||||
UIImage(
|
||||
systemName: multipleSelection ? "square" : "circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
|
||||
for: .normal)
|
||||
setImage(
|
||||
UIImage(
|
||||
systemName: multipleSelection ? "checkmark.square" : "checkmark.circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
|
||||
for: .selected)
|
||||
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
|
||||
heightAnchor.constraint(equalTo: titleLabel!.heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private extension PollOptionButton {
|
||||
static let titleEdgeInsets = UIEdgeInsets(top: 0, left: .compactSpacing, bottom: 0, right: .compactSpacing)
|
||||
}
|
85
Views/PollResultView.swift
Normal file
85
Views/PollResultView.swift
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import UIKit
|
||||
|
||||
class PollResultView: UIView {
|
||||
private let verticalStackView = UIStackView()
|
||||
private let horizontalStackView = UIStackView()
|
||||
private let titleLabel = UILabel()
|
||||
private let percentLabel = UILabel()
|
||||
private let percentView = UIProgressView()
|
||||
|
||||
init(option: Poll.Option, emoji: [Emoji], selected: Bool, multipleSelection: Bool, votersCount: Int) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(verticalStackView)
|
||||
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
verticalStackView.axis = .vertical
|
||||
verticalStackView.spacing = .compactSpacing
|
||||
|
||||
verticalStackView.addArrangedSubview(horizontalStackView)
|
||||
horizontalStackView.spacing = .compactSpacing
|
||||
|
||||
verticalStackView.addArrangedSubview(percentView)
|
||||
|
||||
if selected {
|
||||
let imageView = UIImageView(
|
||||
image: UIImage(
|
||||
systemName: multipleSelection ? "checkmark.square" : "checkmark.circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)))
|
||||
|
||||
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||
horizontalStackView.addArrangedSubview(imageView)
|
||||
}
|
||||
|
||||
horizontalStackView.addArrangedSubview(titleLabel)
|
||||
titleLabel.font = .preferredFont(forTextStyle: .callout)
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
titleLabel.numberOfLines = 0
|
||||
|
||||
horizontalStackView.addArrangedSubview(percentLabel)
|
||||
percentLabel.font = .preferredFont(forTextStyle: .callout)
|
||||
percentLabel.adjustsFontForContentSizeCategory = true
|
||||
percentLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
let attributedTitle = NSMutableAttributedString(string: option.title)
|
||||
|
||||
attributedTitle.insert(emoji: emoji, view: titleLabel)
|
||||
attributedTitle.resizeAttachments(toLineHeight: titleLabel.font.lineHeight)
|
||||
titleLabel.attributedText = attributedTitle
|
||||
|
||||
let percent: Float
|
||||
|
||||
if votersCount == 0 {
|
||||
percent = 0
|
||||
} else {
|
||||
percent = Float(option.votesCount) / Float(votersCount)
|
||||
}
|
||||
|
||||
percentLabel.text = Self.percentFormatter.string(from: NSNumber(value: percent))
|
||||
percentView.progress = percent
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
verticalStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private extension PollResultView {
|
||||
private static var percentFormatter: NumberFormatter = {
|
||||
let percentageFormatter = NumberFormatter()
|
||||
|
||||
percentageFormatter.numberStyle = .percent
|
||||
|
||||
return percentageFormatter
|
||||
}()
|
||||
}
|
156
Views/PollView.swift
Normal file
156
Views/PollView.swift
Normal file
|
@ -0,0 +1,156 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class PollView: UIView {
|
||||
private let stackView = UIStackView()
|
||||
private let voteButtonStackView = UIStackView()
|
||||
private let bottomStackView = UIStackView()
|
||||
private let voteButton = UIButton(type: .system)
|
||||
private let refreshButton = UIButton(type: .system)
|
||||
private let refreshDividerLabel = UILabel()
|
||||
private let votesCountLabel = UILabel()
|
||||
private let votesCountDividerLabel = UILabel()
|
||||
private let expiryLabel = UILabel()
|
||||
private var selectionCancellable: AnyCancellable?
|
||||
|
||||
var viewModel: StatusViewModel? {
|
||||
didSet {
|
||||
for view in stackView.arrangedSubviews {
|
||||
stackView.removeArrangedSubview(view)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
guard let viewModel = viewModel else {
|
||||
selectionCancellable = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !viewModel.isPollExpired, !viewModel.hasVotedInPoll {
|
||||
for (index, option) in viewModel.pollOptions.enumerated() {
|
||||
let button = PollOptionButton(
|
||||
title: option.title,
|
||||
emoji: viewModel.pollEmoji,
|
||||
multipleSelection: viewModel.isPollMultipleSelection)
|
||||
|
||||
button.addAction(
|
||||
UIAction { _ in
|
||||
if viewModel.pollOptionSelections.contains(index) {
|
||||
viewModel.pollOptionSelections.remove(index)
|
||||
} else if viewModel.isPollMultipleSelection {
|
||||
viewModel.pollOptionSelections.insert(index)
|
||||
} else {
|
||||
viewModel.pollOptionSelections = [index]
|
||||
}
|
||||
},
|
||||
for: .touchUpInside)
|
||||
|
||||
stackView.addArrangedSubview(button)
|
||||
}
|
||||
} else {
|
||||
for (index, option) in viewModel.pollOptions.enumerated() {
|
||||
let resultView = PollResultView(
|
||||
option: option,
|
||||
emoji: viewModel.pollEmoji,
|
||||
selected: viewModel.pollOwnVotes.contains(index),
|
||||
multipleSelection: viewModel.isPollMultipleSelection,
|
||||
votersCount: viewModel.pollVotersCount)
|
||||
|
||||
stackView.addArrangedSubview(resultView)
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.isPollExpired, !viewModel.hasVotedInPoll {
|
||||
stackView.addArrangedSubview(voteButtonStackView)
|
||||
|
||||
selectionCancellable = viewModel.$pollOptionSelections.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
for (index, view) in self.stackView.arrangedSubviews.enumerated() {
|
||||
(view as? UIButton)?.isSelected = $0.contains(index)
|
||||
}
|
||||
|
||||
self.voteButton.isEnabled = !$0.isEmpty
|
||||
}
|
||||
} else {
|
||||
selectionCancellable = nil
|
||||
}
|
||||
|
||||
stackView.addArrangedSubview(bottomStackView)
|
||||
|
||||
votesCountLabel.text = String.localizedStringWithFormat(
|
||||
NSLocalizedString("status.poll.participation-count", comment: ""),
|
||||
viewModel.pollVotersCount)
|
||||
|
||||
if !viewModel.isPollExpired, let pollTimeLeft = viewModel.pollTimeLeft {
|
||||
expiryLabel.text = String.localizedStringWithFormat(
|
||||
NSLocalizedString("status.poll.time-left", comment: ""),
|
||||
pollTimeLeft)
|
||||
refreshButton.isHidden = false
|
||||
} else {
|
||||
expiryLabel.text = NSLocalizedString("status.poll.closed", comment: "")
|
||||
refreshButton.isHidden = true
|
||||
}
|
||||
|
||||
refreshDividerLabel.isHidden = refreshButton.isHidden
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension PollView {
|
||||
func initialSetup() {
|
||||
addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = .defaultSpacing
|
||||
|
||||
voteButtonStackView.addArrangedSubview(voteButton)
|
||||
voteButtonStackView.addArrangedSubview(UIView())
|
||||
|
||||
voteButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
voteButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
voteButton.setTitle(NSLocalizedString("status.poll.vote", comment: ""), for: .normal)
|
||||
voteButton.addAction(UIAction { [weak self] _ in self?.viewModel?.vote() }, for: .touchUpInside)
|
||||
|
||||
bottomStackView.spacing = .compactSpacing
|
||||
|
||||
bottomStackView.addArrangedSubview(refreshButton)
|
||||
refreshButton.titleLabel?.font = .preferredFont(forTextStyle: .caption1)
|
||||
refreshButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
refreshButton.setTitle(NSLocalizedString("status.poll.refresh", comment: ""), for: .normal)
|
||||
refreshButton.addAction(UIAction { [weak self] _ in self?.viewModel?.refreshPoll() }, for: .touchUpInside)
|
||||
|
||||
for label in [refreshDividerLabel, votesCountLabel, votesCountDividerLabel, expiryLabel] {
|
||||
bottomStackView.addArrangedSubview(label)
|
||||
label.font = .preferredFont(forTextStyle: .caption1)
|
||||
label.textColor = .secondaryLabel
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
||||
refreshDividerLabel.text = "•"
|
||||
votesCountDividerLabel.text = "•"
|
||||
|
||||
bottomStackView.addArrangedSubview(UIView())
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ final class StatusView: UIView {
|
|||
let toggleShowContentButton = UIButton(type: .system)
|
||||
let contentTextView = TouchFallthroughTextView()
|
||||
let attachmentsView = StatusAttachmentsView()
|
||||
let pollView = PollView()
|
||||
let cardView = CardView()
|
||||
let contextParentTimeLabel = UILabel()
|
||||
let timeApplicationDividerLabel = UILabel()
|
||||
|
@ -163,6 +164,8 @@ private extension StatusView {
|
|||
|
||||
mainStackView.addArrangedSubview(attachmentsView)
|
||||
|
||||
mainStackView.addArrangedSubview(pollView)
|
||||
|
||||
cardView.button.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
guard
|
||||
|
@ -407,6 +410,9 @@ private extension StatusView {
|
|||
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
|
||||
attachmentsView.viewModel = viewModel
|
||||
|
||||
pollView.isHidden = viewModel.pollOptions.count == 0
|
||||
pollView.viewModel = viewModel
|
||||
|
||||
cardView.viewModel = viewModel.cardViewModel
|
||||
cardView.isHidden = viewModel.cardViewModel == nil
|
||||
|
||||
|
|
Loading…
Reference in a new issue