feat: implement multiple poll
This commit is contained in:
parent
d79666679a
commit
0df1a57865
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
if poll.multiple {
|
||||||
|
guard !option.poll.expired, !didVotedRemote else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
|
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
|
||||||
return nil
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue