feat: implement multiple poll

This commit is contained in:
CMK 2021-03-05 15:53:36 +08:00
parent d79666679a
commit 0df1a57865
5 changed files with 117 additions and 22 deletions

View File

@ -245,7 +245,6 @@ extension StatusSection {
cell.statusView.pollTableView.isHidden = false cell.statusView.pollTableView.isHidden = false
cell.statusView.pollStatusStackView.isHidden = false cell.statusView.pollStatusStackView.isHidden = false
cell.statusView.pollVoteButton.isHidden = !poll.multiple
cell.statusView.pollVoteCountLabel.text = { cell.statusView.pollVoteCountLabel.text = {
if poll.multiple { if poll.multiple {
let count = poll.votersCount?.intValue ?? 0 let count = poll.votersCount?.intValue ?? 0
@ -279,7 +278,14 @@ extension StatusSection {
} }
cell.statusView.pollTableView.allowsSelection = !poll.expired cell.statusView.pollTableView.allowsSelection = !poll.expired
cell.statusView.pollTableView.allowsMultipleSelection = poll.multiple
let votedOptions = poll.options.filter { option in
(option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
}
let didVotedLocal = !votedOptions.isEmpty
let didVotedRemote = (poll.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
cell.statusView.pollVoteButton.isEnabled = didVotedLocal
cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired)
cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource( cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource(
for: cell.statusView.pollTableView, for: cell.statusView.pollTableView,
@ -288,21 +294,18 @@ extension StatusSection {
var snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>() var snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
let votedOptions = poll.options.filter { option in
(option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
}
let isPollVoted = (poll.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
let pollItems = poll.options let pollItems = poll.options
.sorted(by: { $0.index.intValue < $1.index.intValue }) .sorted(by: { $0.index.intValue < $1.index.intValue })
.map { option -> PollItem in .map { option -> PollItem in
let attribute: PollItem.Attribute = { let attribute: PollItem.Attribute = {
let selectState: PollItem.Attribute.SelectState = { let selectState: PollItem.Attribute.SelectState = {
// make isPollVoted check later to make the local change possible // check didVotedRemote later to make the local change possible
if !votedOptions.isEmpty { if !votedOptions.isEmpty {
return votedOptions.contains(option) ? .on : .off return votedOptions.contains(option) ? .on : .off
} else if poll.expired { } else if poll.expired {
return .none return .none
} else if isPollVoted, votedOptions.isEmpty { } else if didVotedRemote, votedOptions.isEmpty {
return .none return .none
} else { } else {
return .off return .off
@ -312,7 +315,7 @@ extension StatusSection {
var needsReveal: Bool var needsReveal: Bool
if poll.expired { if poll.expired {
needsReveal = true needsReveal = true
} else if isPollVoted { } else if didVotedRemote {
needsReveal = true needsReveal = true
} else { } else {
needsReveal = false needsReveal = false

View File

@ -74,6 +74,45 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
// MARK: - PollTableView // MARK: - PollTableView
extension StatusTableViewCellDelegate where Self: StatusProvider { extension StatusTableViewCellDelegate where Self: StatusProvider {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
toot(for: cell, indexPath: nil)
.receive(on: DispatchQueue.main)
.setFailureType(to: Error.self)
.compactMap { toot -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
guard let toot = (toot?.reblog ?? toot) else { return nil }
guard let poll = toot.poll else { return nil }
let votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
let choices = votedOptions.map { $0.index.intValue }
let domain = poll.toot.domain
button.isEnabled = false
return self.context.apiService.vote(
domain: domain,
pollID: poll.id,
pollObjectID: poll.objectID,
choices: choices,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
}
.switchToLatest()
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
// TODO: handle error
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: multiple vote fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
button.isEnabled = true
case .finished:
break
}
}, receiveValue: { response in
// do nothing
})
.store(in: &context.disposeBag)
}
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath) { func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let activeMastodonAuthentication = context.authenticationService.activeMastodonAuthentication.value else { return } guard let activeMastodonAuthentication = context.authenticationService.activeMastodonAuthentication.value else { return }
@ -83,16 +122,37 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
guard case let .opion(objectID, attribute) = item else { return } guard case let .opion(objectID, attribute) = item else { return }
guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return } guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return }
let domain = option.poll.toot.domain let poll = option.poll
let pollObjectID = option.poll.objectID let pollObjectID = option.poll.objectID
let domain = poll.toot.domain
if option.poll.multiple { if poll.multiple {
var choices: [Int] = [] var votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
if votedOptions.contains(option) {
votedOptions.remove(option)
} else {
votedOptions.insert(option)
}
let choices = votedOptions.map { $0.index.intValue }
context.apiService.vote(
pollObjectID: option.poll.objectID,
mastodonUserObjectID: activeMastodonAuthentication.user.objectID,
choices: choices
)
.handleEvents(receiveOutput: { _ in
// TODO: add haptic
})
.receive(on: DispatchQueue.main)
.sink { completion in
// Do nothing
} receiveValue: { _ in
// Do nothing
}
.store(in: &context.disposeBag)
} else { } else {
let choices = [option.index.intValue] let choices = [option.index.intValue]
context.apiService.vote( context.apiService.vote(
pollObjectID: option.poll.objectID, pollObjectID: pollObjectID,
mastodonUserObjectID: activeMastodonAuthentication.user.objectID, mastodonUserObjectID: activeMastodonAuthentication.user.objectID,
choices: [option.index.intValue] choices: [option.index.intValue]
) )

View File

@ -13,6 +13,7 @@ import AlamofireImage
protocol StatusViewDelegate: class { protocol StatusViewDelegate: class {
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
} }
final class StatusView: UIView { final class StatusView: UIView {
@ -138,11 +139,12 @@ final class StatusView: UIView {
}() }()
let pollVoteButton: UIButton = { let pollVoteButton: UIButton = {
let button = HitTestExpandedButton() let button = HitTestExpandedButton()
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .regular)) button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal) button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal) button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal)
button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted) button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted)
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled) button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
button.isEnabled = false
return button return button
}() }()
@ -350,6 +352,7 @@ extension StatusView {
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside) contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
} }
} }
@ -385,10 +388,17 @@ extension StatusView {
} }
extension StatusView { extension StatusView {
@objc private func contentWarningActionButtonPressed(_ sender: UIButton) { @objc private func contentWarningActionButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, contentWarningActionButtonPressed: sender) delegate?.statusView(self, contentWarningActionButtonPressed: sender)
} }
@objc private func pollVoteButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, pollVoteButtonPressed: sender)
}
} }
// MARK: - AvatarConfigurableView // MARK: - AvatarConfigurableView

View File

@ -13,15 +13,16 @@ import CoreData
import CoreDataStack import CoreDataStack
protocol StatusTableViewCellDelegate: class { protocol StatusTableViewCellDelegate: class {
var context: AppContext! { get} var context: AppContext! { get }
var managedObjectContext: NSManagedObjectContext { get } var managedObjectContext: NSManagedObjectContext { get }
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
} }
final class StatusTableViewCell: UITableViewCell { final class StatusTableViewCell: UITableViewCell {
@ -101,6 +102,7 @@ extension StatusTableViewCell {
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
extension StatusTableViewCell: UITableViewDelegate { extension StatusTableViewCell: UITableViewDelegate {
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource { if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource {
var pollID: String? var pollID: String?
@ -115,6 +117,7 @@ extension StatusTableViewCell: UITableViewDelegate {
pollID = option.poll.id pollID = option.poll.id
return !option.poll.expired return !option.poll.expired
} else { } else {
assertionFailure()
return true return true
} }
} }
@ -143,20 +146,31 @@ extension StatusTableViewCell: UITableViewDelegate {
(option.votedBy ?? Set()).map { $0.id }.contains(userID) (option.votedBy ?? Set()).map { $0.id }.contains(userID)
} }
let didVotedLocal = !votedOptions.isEmpty let didVotedLocal = !votedOptions.isEmpty
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
return nil if poll.multiple {
guard !option.poll.expired, !didVotedRemote else {
return nil
}
} else {
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
return nil
}
} }
return indexPath return indexPath
} else { } else {
assertionFailure()
return indexPath return indexPath
} }
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView === statusView.pollTableView { if tableView === statusView.pollTableView {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
delegate?.statusTableViewCell(self, pollTableView: statusView.pollTableView, didSelectRowAt: indexPath) delegate?.statusTableViewCell(self, pollTableView: statusView.pollTableView, didSelectRowAt: indexPath)
} else {
assertionFailure()
} }
} }
@ -164,9 +178,15 @@ extension StatusTableViewCell: UITableViewDelegate {
// MARK: - StatusViewDelegate // MARK: - StatusViewDelegate
extension StatusTableViewCell: StatusViewDelegate { extension StatusTableViewCell: StatusViewDelegate {
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button) delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
} }
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
}
} }
// MARK: - MosaicImageViewDelegate // MARK: - MosaicImageViewDelegate

View File

@ -101,11 +101,13 @@ extension APIService {
let votedOptions = poll.options.filter { option in let votedOptions = poll.options.filter { option in
(option.votedBy ?? Set()).map { $0.id }.contains(mastodonUser.id) (option.votedBy ?? Set()).map { $0.id }.contains(mastodonUser.id)
} }
guard votedOptions.isEmpty else {
// if did voted. Do not allow vote again if !poll.multiple, !votedOptions.isEmpty {
// if did voted for single poll. Do not allow vote again
didVotedLocal = true didVotedLocal = true
return return
} }
for option in options { for option in options {
let voted = choices.contains(option.index.intValue) let voted = choices.contains(option.index.intValue)
option.update(voted: voted, by: mastodonUser) option.update(voted: voted, by: mastodonUser)