mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +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()
|
.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> {
|
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try list.save($0)
|
try list.save($0)
|
||||||
|
|
|
@ -91,7 +91,6 @@
|
||||||
"status.show-more" = "Show More";
|
"status.show-more" = "Show More";
|
||||||
"status.show-less" = "Show Less";
|
"status.show-less" = "Show Less";
|
||||||
"status.poll.vote" = "Vote";
|
"status.poll.vote" = "Vote";
|
||||||
"status.poll.participation-count" = "%ld people";
|
|
||||||
"status.poll.time-left" = "%@ left";
|
"status.poll.time-left" = "%@ left";
|
||||||
"status.poll.refresh" = "Refresh";
|
"status.poll.refresh" = "Refresh";
|
||||||
"status.poll.closed" = "Closed";
|
"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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<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>
|
<key>status.reblogs-count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<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 */; };
|
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5E2540DE3A00B1EBEF /* ZoomDismissalInteractionController.swift */; };
|
||||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D5F2540DE3A00B1EBEF /* ZoomTransitionController.swift */; };
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D662540DEB200B1EBEF /* ZoomAnimatableView.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 */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; 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>"; };
|
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -333,6 +339,9 @@
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||||
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
||||||
|
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
||||||
|
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */,
|
||||||
|
D08B8D71254246E200B1EBEF /* PollView.swift */,
|
||||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||||
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
||||||
|
@ -607,13 +616,16 @@
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||||
|
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||||
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
|
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
|
|
|
@ -53,4 +53,20 @@ public extension StatusService {
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
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 {
|
switch item {
|
||||||
case let .status(status, configuration):
|
case let .status(status, configuration):
|
||||||
var viewModel: StatusViewModel
|
let viewModel: StatusViewModel
|
||||||
|
|
||||||
if let cachedViewModel = cachedViewModel as? StatusViewModel {
|
if let cachedViewModel = cachedViewModel as? StatusViewModel {
|
||||||
viewModel = cachedViewModel
|
viewModel = cachedViewModel
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public struct StatusViewModel: CollectionItemViewModel {
|
public final class StatusViewModel: CollectionItemViewModel, ObservableObject {
|
||||||
public let content: NSAttributedString
|
public let content: NSAttributedString
|
||||||
public let contentEmoji: [Emoji]
|
public let contentEmoji: [Emoji]
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
|
@ -15,8 +15,8 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
public let rebloggedByDisplayName: String
|
public let rebloggedByDisplayName: String
|
||||||
public let rebloggedByDisplayNameEmoji: [Emoji]
|
public let rebloggedByDisplayNameEmoji: [Emoji]
|
||||||
public let attachmentViewModels: [AttachmentViewModel]
|
public let attachmentViewModels: [AttachmentViewModel]
|
||||||
public let pollOptionTitles: [String]
|
|
||||||
public let pollEmoji: [Emoji]
|
public let pollEmoji: [Emoji]
|
||||||
|
@Published public var pollOptionSelections = Set<Int>()
|
||||||
public var configuration = CollectionItem.StatusConfiguration.default
|
public var configuration = CollectionItem.StatusConfiguration.default
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
rebloggedByDisplayNameEmoji = statusService.status.account.emojis
|
||||||
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
attachmentViewModels = statusService.status.displayStatus.mediaAttachments
|
||||||
.map { AttachmentViewModel(attachment: $0, status: statusService.status, identification: identification) }
|
.map { AttachmentViewModel(attachment: $0, status: statusService.status, identification: identification) }
|
||||||
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
|
|
||||||
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
|
||||||
events = eventsSubject.eraseToAnyPublisher()
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -112,6 +111,30 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
var sharingURL: URL? { statusService.status.displayStatus.url }
|
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? {
|
var cardViewModel: CardViewModel? {
|
||||||
if let card = statusService.status.displayStatus.card {
|
if let card = statusService.status.displayStatus.card {
|
||||||
return CardViewModel(card: card)
|
return CardViewModel(card: card)
|
||||||
|
@ -191,6 +214,20 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
eventsSubject.send(Just(.share(url)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
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 {
|
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 toggleShowContentButton = UIButton(type: .system)
|
||||||
let contentTextView = TouchFallthroughTextView()
|
let contentTextView = TouchFallthroughTextView()
|
||||||
let attachmentsView = StatusAttachmentsView()
|
let attachmentsView = StatusAttachmentsView()
|
||||||
|
let pollView = PollView()
|
||||||
let cardView = CardView()
|
let cardView = CardView()
|
||||||
let contextParentTimeLabel = UILabel()
|
let contextParentTimeLabel = UILabel()
|
||||||
let timeApplicationDividerLabel = UILabel()
|
let timeApplicationDividerLabel = UILabel()
|
||||||
|
@ -163,6 +164,8 @@ private extension StatusView {
|
||||||
|
|
||||||
mainStackView.addArrangedSubview(attachmentsView)
|
mainStackView.addArrangedSubview(attachmentsView)
|
||||||
|
|
||||||
|
mainStackView.addArrangedSubview(pollView)
|
||||||
|
|
||||||
cardView.button.addAction(
|
cardView.button.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
guard
|
guard
|
||||||
|
@ -407,6 +410,9 @@ private extension StatusView {
|
||||||
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
|
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
|
||||||
attachmentsView.viewModel = viewModel
|
attachmentsView.viewModel = viewModel
|
||||||
|
|
||||||
|
pollView.isHidden = viewModel.pollOptions.count == 0
|
||||||
|
pollView.viewModel = viewModel
|
||||||
|
|
||||||
cardView.viewModel = viewModel.cardViewModel
|
cardView.viewModel = viewModel.cardViewModel
|
||||||
cardView.isHidden = viewModel.cardViewModel == nil
|
cardView.isHidden = viewModel.cardViewModel == nil
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue