From 0df1a57865fa9f8675ec514fb3cd763e339997b8 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 Mar 2021 15:53:36 +0800 Subject: [PATCH] feat: implement multiple poll --- .../Diffiable/Section/StatusSection.swift | 21 +++--- ...Provider+StatusTableViewCellDelegate.swift | 70 +++++++++++++++++-- .../Scene/Share/View/Content/StatusView.swift | 12 +++- .../TableviewCell/StatusTableViewCell.swift | 30 ++++++-- .../Service/APIService/APIService+Poll.swift | 6 +- 5 files changed, 117 insertions(+), 22 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4863f475..5f9d43ed 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -245,7 +245,6 @@ extension StatusSection { cell.statusView.pollTableView.isHidden = false cell.statusView.pollStatusStackView.isHidden = false - cell.statusView.pollVoteButton.isHidden = !poll.multiple cell.statusView.pollVoteCountLabel.text = { if poll.multiple { let count = poll.votersCount?.intValue ?? 0 @@ -279,7 +278,14 @@ extension StatusSection { } 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( for: cell.statusView.pollTableView, @@ -288,21 +294,18 @@ extension StatusSection { var snapshot = NSDiffableDataSourceSnapshot() 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 .sorted(by: { $0.index.intValue < $1.index.intValue }) .map { option -> PollItem in let attribute: PollItem.Attribute = { 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 { return votedOptions.contains(option) ? .on : .off } else if poll.expired { return .none - } else if isPollVoted, votedOptions.isEmpty { + } else if didVotedRemote, votedOptions.isEmpty { return .none } else { return .off @@ -312,7 +315,7 @@ extension StatusSection { var needsReveal: Bool if poll.expired { needsReveal = true - } else if isPollVoted { + } else if didVotedRemote { needsReveal = true } else { needsReveal = false diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index d62fb6cc..cd4e5160 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -74,6 +74,45 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { // MARK: - PollTableView 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, 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) { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.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 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 domain = poll.toot.domain - if option.poll.multiple { - var choices: [Int] = [] - + if poll.multiple { + 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 { let choices = [option.index.intValue] context.apiService.vote( - pollObjectID: option.poll.objectID, + pollObjectID: pollObjectID, mastodonUserObjectID: activeMastodonAuthentication.user.objectID, choices: [option.index.intValue] ) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index f6095db0..c1f3cb3d 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -13,6 +13,7 @@ import AlamofireImage protocol StatusViewDelegate: class { func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) + func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) } final class StatusView: UIView { @@ -138,11 +139,12 @@ final class StatusView: UIView { }() let pollVoteButton: UIButton = { 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.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.disabled.color, for: .disabled) + button.isEnabled = false return button }() @@ -350,6 +352,7 @@ extension StatusView { statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false 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 { + @objc private func contentWarningActionButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) 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 diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 64ffb167..13c3afba 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -13,15 +13,16 @@ import CoreData import CoreDataStack protocol StatusTableViewCellDelegate: class { - var context: AppContext! { get} + var context: AppContext! { get } var managedObjectContext: NSManagedObjectContext { get } func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) 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, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) + + func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath) - } final class StatusTableViewCell: UITableViewCell { @@ -101,6 +102,7 @@ extension StatusTableViewCell { // MARK: - UITableViewDelegate extension StatusTableViewCell: UITableViewDelegate { + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource { var pollID: String? @@ -115,6 +117,7 @@ extension StatusTableViewCell: UITableViewDelegate { pollID = option.poll.id return !option.poll.expired } else { + assertionFailure() return true } } @@ -143,20 +146,31 @@ extension StatusTableViewCell: UITableViewDelegate { (option.votedBy ?? Set()).map { $0.id }.contains(userID) } 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 } else { + assertionFailure() return indexPath } } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 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) + } else { + assertionFailure() } } @@ -164,9 +178,15 @@ extension StatusTableViewCell: UITableViewDelegate { // MARK: - StatusViewDelegate extension StatusTableViewCell: StatusViewDelegate { + func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button) } + + func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { + delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button) + } + } // MARK: - MosaicImageViewDelegate diff --git a/Mastodon/Service/APIService/APIService+Poll.swift b/Mastodon/Service/APIService/APIService+Poll.swift index 350da97c..0b240466 100644 --- a/Mastodon/Service/APIService/APIService+Poll.swift +++ b/Mastodon/Service/APIService/APIService+Poll.swift @@ -101,11 +101,13 @@ extension APIService { let votedOptions = poll.options.filter { option in (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 return } + for option in options { let voted = choices.contains(option.index.intValue) option.update(voted: voted, by: mastodonUser)