mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
feat: [WIP] make the vote poll logic works
This commit is contained in:
parent
028f3a9404
commit
06aac878c8
@ -62,7 +62,7 @@ extension PollOption {
|
|||||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !(self.votedBy ?? Set()).contains(by) {
|
if (self.votedBy ?? Set()).contains(by) {
|
||||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
|
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,20 @@
|
|||||||
"ERR_INCLUSION": "is not a supported value"
|
"ERR_INCLUSION": "is not a supported value"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
|
"common": {
|
||||||
|
"please_try_again": "Please try again.",
|
||||||
|
"please_try_again_later": "Please try again later."
|
||||||
|
},
|
||||||
"sign_up_failure": {
|
"sign_up_failure": {
|
||||||
"title": "Sign Up Failure"
|
"title": "Sign Up Failure"
|
||||||
},
|
},
|
||||||
"server_error": {
|
"server_error": {
|
||||||
"title": "Server Error"
|
"title": "Server Error"
|
||||||
|
},
|
||||||
|
"vote_failure": {
|
||||||
|
"title": "Vote Failure",
|
||||||
|
"poll_expired": "The poll has expired"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
@ -284,13 +284,13 @@ extension StatusSection {
|
|||||||
.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 = {
|
||||||
if isPollVoted {
|
// make isPollVoted check later to make only local change possible
|
||||||
guard !votedOptions.isEmpty else {
|
if !votedOptions.isEmpty {
|
||||||
return .none
|
|
||||||
}
|
|
||||||
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 {
|
||||||
|
return .none
|
||||||
} else {
|
} else {
|
||||||
return .off
|
return .off
|
||||||
}
|
}
|
||||||
@ -302,6 +302,8 @@ extension StatusSection {
|
|||||||
return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue)
|
return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue)
|
||||||
}()
|
}()
|
||||||
let voted = votedOptions.isEmpty ? true : votedOptions.contains(option)
|
let voted = votedOptions.isEmpty ? true : votedOptions.contains(option)
|
||||||
|
// let voted = true
|
||||||
|
// let percentage: Double = Double.random(in: 0..<1)
|
||||||
return .reveal(voted: voted, percentage: percentage)
|
return .reveal(voted: voted, percentage: percentage)
|
||||||
}()
|
}()
|
||||||
return PollItem.Attribute(selectState: selectState, voteState: voteState)
|
return PollItem.Attribute(selectState: selectState, voteState: voteState)
|
||||||
|
@ -13,6 +13,12 @@ internal enum L10n {
|
|||||||
|
|
||||||
internal enum Common {
|
internal enum Common {
|
||||||
internal enum Alerts {
|
internal enum Alerts {
|
||||||
|
internal enum Common {
|
||||||
|
/// Please try again.
|
||||||
|
internal static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain")
|
||||||
|
/// Please try again later.
|
||||||
|
internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater")
|
||||||
|
}
|
||||||
internal enum ServerError {
|
internal enum ServerError {
|
||||||
/// Server Error
|
/// Server Error
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
||||||
@ -21,6 +27,12 @@ internal enum L10n {
|
|||||||
/// Sign Up Failure
|
/// Sign Up Failure
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
|
||||||
}
|
}
|
||||||
|
internal enum VoteFailure {
|
||||||
|
/// The poll has expired
|
||||||
|
internal static let pollExpired = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollExpired")
|
||||||
|
/// Vote Failure
|
||||||
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.VoteFailure.Title")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
internal enum Controls {
|
internal enum Controls {
|
||||||
internal enum Actions {
|
internal enum Actions {
|
||||||
|
@ -39,6 +39,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MosciaImageViewContainerDelegate
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||||
@ -69,3 +70,37 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - PollTableView
|
||||||
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
guard let activeMastodonAuthentication = context.authenticationService.activeMastodonAuthentication.value else { return }
|
||||||
|
|
||||||
|
guard let diffableDataSource = cell.statusView.pollTableViewDataSource else { return }
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||||
|
guard case let .opion(objectID, attribute) = item else { return }
|
||||||
|
guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return }
|
||||||
|
|
||||||
|
|
||||||
|
if option.poll.multiple {
|
||||||
|
var choices: [Int] = []
|
||||||
|
|
||||||
|
} else {
|
||||||
|
context.apiService.vote(
|
||||||
|
pollObjectID: option.poll.objectID,
|
||||||
|
mastodonUserObjectID: activeMastodonAuthentication.user.objectID,
|
||||||
|
choices: [option.index.intValue]
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
|
||||||
|
} receiveValue: { pollID in
|
||||||
|
|
||||||
|
}
|
||||||
|
.store(in: &context.disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -33,7 +33,12 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
|
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
|
||||||
guard timeIntervalSinceUpdate > 60 else {
|
#if DEBUG
|
||||||
|
let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
|
||||||
|
#else
|
||||||
|
let autoRefreshTimeInterval: TimeInterval = 60
|
||||||
|
#endif
|
||||||
|
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s updated in the %.2fs. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id, timeIntervalSinceUpdate)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s updated in the %.2fs. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id, timeIntervalSinceUpdate)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
|
||||||
|
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
||||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||||
|
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||||
|
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
||||||
"Common.Controls.Actions.Add" = "Add";
|
"Common.Controls.Actions.Add" = "Add";
|
||||||
"Common.Controls.Actions.Back" = "Back";
|
"Common.Controls.Actions.Back" = "Back";
|
||||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||||
|
@ -51,8 +51,10 @@ extension VoteProgressStripView {
|
|||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] progress in
|
.sink { [weak self] progress in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
UIView.animate(withDuration: 0.33) {
|
||||||
self.updateLayerPath()
|
self.updateLayerPath()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,11 +13,15 @@ import CoreData
|
|||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
|
||||||
protocol StatusTableViewCellDelegate: class {
|
protocol StatusTableViewCellDelegate: class {
|
||||||
|
var context: AppContext! { get}
|
||||||
var managedObjectContext: NSManagedObjectContext { get }
|
var managedObjectContext: NSManagedObjectContext { get }
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
|
||||||
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, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StatusTableViewCell: UITableViewCell {
|
final class StatusTableViewCell: UITableViewCell {
|
||||||
@ -110,6 +114,44 @@ extension StatusTableViewCell: UITableViewDelegate {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||||
|
if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||||
|
|
||||||
|
guard let context = delegate?.context else { return nil }
|
||||||
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .opion(objectID, _) = item,
|
||||||
|
let option = delegate?.managedObjectContext.object(with: objectID) as? PollOption else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let poll = option.poll
|
||||||
|
|
||||||
|
// disallow select when: poll expired OR user voted remote OR user voted local
|
||||||
|
let userID = activeMastodonAuthenticationBox.userID
|
||||||
|
let didVotedRemote = (option.poll.votedBy ?? Set()).contains(where: { $0.id == userID })
|
||||||
|
let votedOptions = poll.options.filter { option in
|
||||||
|
(option.votedBy ?? Set()).map { $0.id }.contains(userID)
|
||||||
|
}
|
||||||
|
let didVotedLocal = !votedOptions.isEmpty
|
||||||
|
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexPath
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
delegate?.statusTableViewCell(self, pollTableView: statusView.pollTableView, didSelectRowAt: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusViewDelegate
|
// MARK: - StatusViewDelegate
|
||||||
|
@ -21,6 +21,8 @@ extension APIService {
|
|||||||
case badResponse
|
case badResponse
|
||||||
case requestThrottle
|
case requestThrottle
|
||||||
|
|
||||||
|
case voteExpiredPoll
|
||||||
|
|
||||||
// Server API error
|
// Server API error
|
||||||
case mastodonAPIError(Mastodon.API.Error)
|
case mastodonAPIError(Mastodon.API.Error)
|
||||||
}
|
}
|
||||||
@ -44,6 +46,7 @@ extension APIService.APIError: LocalizedError {
|
|||||||
case .badRequest: return "Bad Request"
|
case .badRequest: return "Bad Request"
|
||||||
case .badResponse: return "Bad Response"
|
case .badResponse: return "Bad Response"
|
||||||
case .requestThrottle: return "Request Throttled"
|
case .requestThrottle: return "Request Throttled"
|
||||||
|
case .voteExpiredPoll: return L10n.Common.Alerts.VoteFailure.title
|
||||||
case .mastodonAPIError(let error):
|
case .mastodonAPIError(let error):
|
||||||
guard let responseError = error.mastodonError else {
|
guard let responseError = error.mastodonError else {
|
||||||
guard error.httpResponseStatus != .ok else {
|
guard error.httpResponseStatus != .ok else {
|
||||||
@ -62,6 +65,7 @@ extension APIService.APIError: LocalizedError {
|
|||||||
case .badRequest: return "Request invalid."
|
case .badRequest: return "Request invalid."
|
||||||
case .badResponse: return "Response invalid."
|
case .badResponse: return "Response invalid."
|
||||||
case .requestThrottle: return "Request too frequency."
|
case .requestThrottle: return "Request too frequency."
|
||||||
|
case .voteExpiredPoll: return L10n.Common.Alerts.VoteFailure.pollExpired
|
||||||
case .mastodonAPIError(let error):
|
case .mastodonAPIError(let error):
|
||||||
guard let responseError = error.mastodonError else {
|
guard let responseError = error.mastodonError else {
|
||||||
return nil
|
return nil
|
||||||
@ -73,9 +77,10 @@ extension APIService.APIError: LocalizedError {
|
|||||||
var helpAnchor: String? {
|
var helpAnchor: String? {
|
||||||
switch errorReason {
|
switch errorReason {
|
||||||
case .authenticationMissing: return "Please request after authenticated."
|
case .authenticationMissing: return "Please request after authenticated."
|
||||||
case .badRequest: return "Please try again."
|
case .badRequest: return L10n.Common.Alerts.Common.pleaseTryAgain
|
||||||
case .badResponse: return "Please try again."
|
case .badResponse: return L10n.Common.Alerts.Common.pleaseTryAgain
|
||||||
case .requestThrottle: return "Please try again later."
|
case .requestThrottle: return L10n.Common.Alerts.Common.pleaseTryAgainLater
|
||||||
|
case .voteExpiredPoll: return nil
|
||||||
case .mastodonAPIError(let error):
|
case .mastodonAPIError(let error):
|
||||||
guard let responseError = error.mastodonError else {
|
guard let responseError = error.mastodonError else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -69,3 +69,127 @@ extension APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
/// vote local
|
||||||
|
/// # Note
|
||||||
|
/// Not mark the poll voted so that view model could know when to reveal the results
|
||||||
|
func vote(
|
||||||
|
pollObjectID: NSManagedObjectID,
|
||||||
|
mastodonUserObjectID: NSManagedObjectID,
|
||||||
|
choices: [Int]
|
||||||
|
) -> AnyPublisher<Mastodon.Entity.Poll.ID, Error> {
|
||||||
|
var _targetPollID: Mastodon.Entity.Poll.ID?
|
||||||
|
var isPollExpired = false
|
||||||
|
var didVotedLocal = false
|
||||||
|
|
||||||
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
let poll = managedObjectContext.object(with: pollObjectID) as! Poll
|
||||||
|
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||||
|
|
||||||
|
_targetPollID = poll.id
|
||||||
|
|
||||||
|
if let expiresAt = poll.expiresAt, Date().timeIntervalSince(expiresAt) > 0 {
|
||||||
|
isPollExpired = true
|
||||||
|
poll.update(expired: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||||
|
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
|
||||||
|
didVotedLocal = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for option in options {
|
||||||
|
let voted = choices.contains(option.index.intValue)
|
||||||
|
option.update(voted: voted, by: mastodonUser)
|
||||||
|
option.didUpdate(at: option.updatedAt) // trigger update without change anything
|
||||||
|
}
|
||||||
|
poll.didUpdate(at: poll.updatedAt) // trigger update without change anything
|
||||||
|
}
|
||||||
|
.tryMap { result in
|
||||||
|
guard !isPollExpired else {
|
||||||
|
throw APIError.explicit(APIError.ErrorReason.voteExpiredPoll)
|
||||||
|
}
|
||||||
|
guard !didVotedLocal else {
|
||||||
|
throw APIError.implicit(APIError.ErrorReason.badRequest)
|
||||||
|
}
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
guard let targetPollID = _targetPollID else {
|
||||||
|
throw APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
return targetPollID
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send vote request to remote
|
||||||
|
func vote(
|
||||||
|
domain: String,
|
||||||
|
pollID: Mastodon.Entity.Poll.ID,
|
||||||
|
pollObjectID: NSManagedObjectID,
|
||||||
|
choices: [Int],
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||||
|
|
||||||
|
let query = Mastodon.API.Polls.VoteQuery(choices: choices)
|
||||||
|
return Mastodon.API.Polls.vote(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
pollID: pollID,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> in
|
||||||
|
let entity = response.value
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
let _requestMastodonUser: MastodonUser? = {
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
guard let requestMastodonUser = _requestMastodonUser else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return }
|
||||||
|
APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate)
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Poll> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import Combine
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Mastodon.API.Favorites {
|
extension Mastodon.API.Favorites {
|
||||||
|
|
||||||
static func favoritesStatusesEndpointURL(domain: String) -> URL {
|
static func favoritesStatusesEndpointURL(domain: String) -> URL {
|
||||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("favourites")
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("favourites")
|
||||||
}
|
}
|
||||||
@ -34,6 +35,8 @@ extension Mastodon.API.Favorites {
|
|||||||
///
|
///
|
||||||
/// Add a status to your favourites list / Remove a status from your favourites list
|
/// Add a status to your favourites list / Remove a status from your favourites list
|
||||||
///
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/3/3
|
/// 2021/3/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
@ -60,6 +63,8 @@ extension Mastodon.API.Favorites {
|
|||||||
///
|
///
|
||||||
/// View who favourited a given status.
|
/// View who favourited a given status.
|
||||||
///
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/3/3
|
/// 2021/3/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
@ -85,6 +90,8 @@ extension Mastodon.API.Favorites {
|
|||||||
///
|
///
|
||||||
/// Using this endpoint to view the favourited list for user
|
/// Using this endpoint to view the favourited list for user
|
||||||
///
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/3/3
|
/// 2021/3/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
@ -104,9 +111,11 @@ extension Mastodon.API.Favorites {
|
|||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Mastodon.API.Favorites {
|
public extension Mastodon.API.Favorites {
|
||||||
|
|
||||||
enum FavoriteKind {
|
enum FavoriteKind {
|
||||||
case create
|
case create
|
||||||
case destroy
|
case destroy
|
||||||
@ -144,4 +153,5 @@ public extension Mastodon.API.Favorites {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,17 @@ extension Mastodon.API.Polls {
|
|||||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func votePollEndpointURL(domain: String, pollID: Mastodon.Entity.Poll.ID) -> URL {
|
||||||
|
let pathComponent = "polls/" + pollID + "/votes"
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
/// View a poll
|
/// View a poll
|
||||||
///
|
///
|
||||||
/// Using this endpoint to view the poll of status
|
/// Using this endpoint to view the poll of status
|
||||||
///
|
///
|
||||||
|
/// - Since: 2.8.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/3/3
|
/// 2021/3/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
@ -28,7 +35,7 @@ extension Mastodon.API.Polls {
|
|||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
/// - pollID: id for poll
|
/// - pollID: id for poll
|
||||||
/// - authorization: User token. Could be nil if status is public
|
/// - authorization: User token. Could be nil if status is public
|
||||||
/// - Returns: `AnyPublisher` contains `Server` nested in the response
|
/// - Returns: `AnyPublisher` contains `Poll` nested in the response
|
||||||
public static func poll(
|
public static func poll(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
@ -48,4 +55,51 @@ extension Mastodon.API.Polls {
|
|||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Vote on a poll
|
||||||
|
///
|
||||||
|
/// Using this endpoint to vote an option of poll
|
||||||
|
///
|
||||||
|
/// - Since: 2.8.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/4
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/polls/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - pollID: id for poll
|
||||||
|
/// - query: `VoteQuery`
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Poll` nested in the response
|
||||||
|
public static func vote(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
pollID: Mastodon.Entity.Poll.ID,
|
||||||
|
query: VoteQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||||
|
let request = Mastodon.API.post(
|
||||||
|
url: votePollEndpointURL(domain: domain, pollID: pollID),
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Poll.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Polls {
|
||||||
|
public struct VoteQuery: Codable, PostQuery {
|
||||||
|
public let choices: [Int]
|
||||||
|
|
||||||
|
public init(choices: [Int]) {
|
||||||
|
self.choices = choices
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user