Refactor Polls to not use Core Data (#1265)
This commit is contained in:
parent
b120d32efa
commit
24e573d9e2
|
@ -159,53 +159,8 @@ extension StatusSection {
|
|||
|
||||
cell.pollOptionView.viewModel.authContext = authContext
|
||||
|
||||
managedObjectContext.performAndWait {
|
||||
guard let option = record.object(in: managedObjectContext) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
cell.pollOptionView.configure(pollOption: record)
|
||||
|
||||
cell.pollOptionView.configure(pollOption: option, status: statusView.viewModel.originalStatus)
|
||||
|
||||
// trigger update if needs
|
||||
let needsUpdatePoll: Bool = {
|
||||
// check first option in poll to trigger update poll only once
|
||||
guard
|
||||
let poll = option.poll,
|
||||
option.index == 0
|
||||
else { return false }
|
||||
|
||||
guard !poll.expired else {
|
||||
return false
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
|
||||
#if DEBUG
|
||||
let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
|
||||
#else
|
||||
let autoRefreshTimeInterval: TimeInterval = 30
|
||||
#endif
|
||||
|
||||
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}()
|
||||
|
||||
if needsUpdatePoll {
|
||||
guard let poll = option.poll else { return }
|
||||
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: poll.objectID)
|
||||
Task { [weak context] in
|
||||
guard let context = context else { return }
|
||||
_ = try await context.apiService.poll(
|
||||
poll: pollRecord,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
} // end managedObjectContext.performAndWait
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
|
|
@ -523,3 +523,70 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
|||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - poll
|
||||
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
pollTableView tableView: UITableView,
|
||||
didSelectRowAt indexPath: IndexPath
|
||||
) {
|
||||
guard let pollTableViewDiffableDataSource = notificationView.statusView.pollTableViewDiffableDataSource else { return }
|
||||
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
guard case let .option(pollOption) = pollItem else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
let poll = pollOption.poll
|
||||
|
||||
if !poll.multiple {
|
||||
poll.options.forEach { $0.isSelected = false }
|
||||
pollOption.isSelected = true
|
||||
} else {
|
||||
pollOption.isSelected.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
notificationView: NotificationView,
|
||||
pollVoteButtonPressed button: UIButton
|
||||
) {
|
||||
guard let pollTableViewDiffableDataSource = notificationView.statusView.pollTableViewDiffableDataSource else { return }
|
||||
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
|
||||
guard case let .option(firstPollOption) = firstPollItem else { return }
|
||||
|
||||
notificationView.statusView.viewModel.isVoting = true
|
||||
|
||||
Task { @MainActor in
|
||||
let poll = firstPollOption.poll
|
||||
|
||||
let choices = poll.options
|
||||
.filter { $0.isSelected == true }
|
||||
.compactMap { poll.options.firstIndex(of: $0) }
|
||||
|
||||
do {
|
||||
let newPoll = try await context.apiService.vote(
|
||||
poll: poll.entity,
|
||||
choices: choices,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
guard let entity = poll.status?.entity else { return }
|
||||
|
||||
let newStatus: MastodonStatus = .fromEntity(entity)
|
||||
newStatus.poll = MastodonPoll(poll: newPoll, status: newStatus)
|
||||
|
||||
self.update(status: newStatus, intent: .pollVote)
|
||||
} catch {
|
||||
notificationView.statusView.viewModel.isVoting = false
|
||||
}
|
||||
|
||||
} // end Task
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import MastodonUI
|
|||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
import LinkPresentation
|
||||
import MastodonSDK
|
||||
|
||||
// MARK: - header
|
||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
@ -264,65 +265,19 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
|
||||
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
|
||||
Task {
|
||||
guard case let .option(pollOption) = pollItem else {
|
||||
assertionFailure("only works for status data provider")
|
||||
return
|
||||
}
|
||||
|
||||
var _poll: ManagedObjectRecord<Poll>?
|
||||
var _isMultiple: Bool?
|
||||
var _choice: Int?
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
|
||||
guard let poll = pollOption.poll else { return }
|
||||
_poll = .init(objectID: poll.objectID)
|
||||
|
||||
_isMultiple = poll.multiple
|
||||
guard !poll.isVoting else { return }
|
||||
let poll = pollOption.poll
|
||||
|
||||
if !poll.multiple {
|
||||
for option in poll.options where option != pollOption {
|
||||
option.update(isSelected: false)
|
||||
poll.options.forEach { $0.isSelected = false }
|
||||
pollOption.isSelected = true
|
||||
} else {
|
||||
pollOption.isSelected.toggle()
|
||||
}
|
||||
|
||||
// mark voting
|
||||
poll.update(isVoting: true)
|
||||
// set choice
|
||||
_choice = Int(pollOption.index)
|
||||
}
|
||||
|
||||
pollOption.update(isSelected: !pollOption.isSelected)
|
||||
poll.update(updatedAt: Date())
|
||||
}
|
||||
|
||||
// Trigger vote API request for
|
||||
guard let poll = _poll,
|
||||
_isMultiple == false,
|
||||
let choice = _choice
|
||||
else { return }
|
||||
|
||||
do {
|
||||
_ = try await context.apiService.vote(
|
||||
poll: poll,
|
||||
choices: [choice],
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
} catch {
|
||||
// restore voting state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard
|
||||
let pollOption = pollOption.object(in: managedObjectContext),
|
||||
let poll = pollOption.poll
|
||||
else { return }
|
||||
poll.update(isVoting: false)
|
||||
}
|
||||
}
|
||||
|
||||
} // end Task
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
|
@ -334,45 +289,30 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
|
||||
guard case let .option(firstPollOption) = firstPollItem else { return }
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
statusView.viewModel.isVoting = true
|
||||
|
||||
Task {
|
||||
var _poll: ManagedObjectRecord<Poll>?
|
||||
var _choices: [Int]?
|
||||
Task { @MainActor in
|
||||
let poll = firstPollOption.poll
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let poll = firstPollOption.object(in: managedObjectContext)?.poll else { return }
|
||||
_poll = .init(objectID: poll.objectID)
|
||||
|
||||
guard poll.multiple else { return }
|
||||
|
||||
// mark voting
|
||||
poll.update(isVoting: true)
|
||||
// set choice
|
||||
_choices = poll.options
|
||||
.filter { $0.isSelected }
|
||||
.map { Int($0.index) }
|
||||
|
||||
poll.update(updatedAt: Date())
|
||||
}
|
||||
|
||||
// Trigger vote API request for
|
||||
guard let poll = _poll,
|
||||
let choices = _choices
|
||||
else { return }
|
||||
let choices = poll.options
|
||||
.filter { $0.isSelected == true }
|
||||
.compactMap { poll.options.firstIndex(of: $0) }
|
||||
|
||||
do {
|
||||
_ = try await context.apiService.vote(
|
||||
poll: poll,
|
||||
let newPoll = try await context.apiService.vote(
|
||||
poll: poll.entity,
|
||||
choices: choices,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox
|
||||
)
|
||||
).value
|
||||
|
||||
guard let entity = poll.status?.entity else { return }
|
||||
|
||||
let newStatus: MastodonStatus = .fromEntity(entity)
|
||||
newStatus.poll = MastodonPoll(poll: newPoll, status: newStatus)
|
||||
|
||||
self.update(status: newStatus, intent: .pollVote)
|
||||
} catch {
|
||||
// restore voting state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let poll = poll.object(in: managedObjectContext) else { return }
|
||||
poll.update(isVoting: false)
|
||||
}
|
||||
statusView.viewModel.isVoting = false
|
||||
}
|
||||
|
||||
} // end Task
|
||||
|
|
|
@ -30,6 +30,8 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg
|
|||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, pollVoteButtonPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
|
@ -71,6 +73,14 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
|
|||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaView: mediaView, didSelectMediaViewAt: index)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, pollTableView: tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, pollVoteButtonPressed: button)
|
||||
}
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||
delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ public protocol NotificationViewDelegate: AnyObject {
|
|||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
|
||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
|
@ -547,11 +548,11 @@ extension NotificationView: StatusViewDelegate {
|
|||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
assertionFailure()
|
||||
delegate?.notificationView(self, statusView: statusView, pollTableView: tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
assertionFailure()
|
||||
delegate?.notificationView(self, statusView: statusView, pollVoteButtonPressed: button)
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||
|
|
|
@ -14,92 +14,55 @@ import MastodonUI
|
|||
import MastodonSDK
|
||||
|
||||
extension PollOptionView {
|
||||
public func configure(pollOption option: PollOption, status: MastodonStatus?) {
|
||||
guard let poll = option.poll else {
|
||||
assertionFailure("PollOption to be configured is expected to be part of Poll with Status")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.objects.insert(option)
|
||||
public func configure(pollOption option: MastodonPollOption) {
|
||||
let poll = option.poll
|
||||
let status = option.poll.status
|
||||
|
||||
// metaContent
|
||||
option.publisher(for: \.title)
|
||||
option.$title
|
||||
.map { title -> MetaContent? in
|
||||
return PlaintextMetaContent(string: title)
|
||||
}
|
||||
.assign(to: \.metaContent, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// percentage
|
||||
Publishers.CombineLatest(
|
||||
poll.publisher(for: \.votersCount),
|
||||
option.publisher(for: \.votesCount)
|
||||
poll.$votersCount,
|
||||
option.$votesCount
|
||||
)
|
||||
.map { pollVotersCount, optionVotesCount -> Double? in
|
||||
guard pollVotersCount > 0, optionVotesCount >= 0 else { return 0 }
|
||||
guard let pollVotersCount, pollVotersCount > 0, let optionVotesCount, optionVotesCount >= 0 else { return 0 }
|
||||
return Double(optionVotesCount) / Double(pollVotersCount)
|
||||
}
|
||||
.assign(to: \.percentage, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// $isExpire
|
||||
poll.publisher(for: \.expired)
|
||||
poll.$expired
|
||||
.assign(to: \.isExpire, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// isMultiple
|
||||
viewModel.isMultiple = poll.multiple
|
||||
|
||||
let optionIndex = option.index
|
||||
let authContext = viewModel.authContext
|
||||
|
||||
let authorDomain = status?.entity.account.domain ?? ""
|
||||
let authorID = status?.entity.account.id ?? ""
|
||||
// isSelect, isPollVoted, isMyPoll
|
||||
Publishers.CombineLatest4(
|
||||
option.publisher(for: \.poll),
|
||||
option.publisher(for: \.votedBy),
|
||||
option.publisher(for: \.isSelected),
|
||||
viewModel.$authContext
|
||||
)
|
||||
.sink { [weak self] poll, optionVotedBy, isSelected, authContext in
|
||||
guard let self = self, let poll = poll else { return }
|
||||
|
||||
let domain = authContext?.mastodonAuthenticationBox.domain ?? ""
|
||||
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
||||
|
||||
let options = poll.options
|
||||
let pollVoteBy = poll.votedBy ?? Set()
|
||||
|
||||
let isMyPoll = authorDomain == domain
|
||||
&& authorID == userID
|
||||
|
||||
let votedOptions = options.filter { option in
|
||||
let votedBy = option.votedBy ?? Set()
|
||||
return votedBy.contains(where: { $0.id == userID && $0.domain == domain })
|
||||
}
|
||||
let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex })
|
||||
let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain })
|
||||
|
||||
let isLocalVotedOption = isSelected
|
||||
|
||||
let isSelect: Bool? = {
|
||||
if isLocalVotedOption {
|
||||
return true
|
||||
} else if !votedOptions.isEmpty {
|
||||
return isRemoteVotedOption ? true : false
|
||||
} else if isRemoteVotedPoll, votedOptions.isEmpty {
|
||||
// the poll voted. But server not mark voted options
|
||||
return nil
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}()
|
||||
self.viewModel.isSelect = isSelect
|
||||
self.viewModel.isPollVoted = isRemoteVotedPoll
|
||||
self.viewModel.isSelect = option.isSelected
|
||||
self.viewModel.isPollVoted = poll.voted == true
|
||||
self.viewModel.isMyPoll = isMyPoll
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// appearance
|
||||
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
|
||||
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
|
||||
})
|
||||
|
||||
// appearance
|
||||
checkmarkBackgroundView.backgroundColor = SystemTheme.tableViewCellSelectionBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,8 +75,6 @@ extension PollOptionView {
|
|||
// show left-hand-side dots, otherwise view looks "incomplete"
|
||||
viewModel.selectState = .off
|
||||
// appearance
|
||||
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
|
||||
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
|
||||
})
|
||||
checkmarkBackgroundView.backgroundColor = SystemTheme.tableViewCellSelectionBackgroundColor
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
|
||||
// sourcery: protocolName = "StatusViewDelegate"
|
||||
// sourcery: replaceOf = "statusView(statusView"
|
||||
|
|
|
@ -86,6 +86,8 @@ extension ThreadViewController: DataSourceProvider {
|
|||
viewModel.handleEdit(status)
|
||||
case .delete:
|
||||
break // this case has already been handled
|
||||
case .pollVote:
|
||||
viewModel.handleEdit(status) // technically the data changed so refresh it to reflect the new data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ final public class MastodonUser: NSManagedObject {
|
|||
@NSManaged public private(set) var reblogged: Set<Status>
|
||||
@NSManaged public private(set) var muted: Set<Status>
|
||||
@NSManaged public private(set) var bookmarked: Set<Status>
|
||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>
|
||||
@NSManaged public private(set) var votePolls: Set<Poll>
|
||||
@NSManaged public private(set) var votePollOptions: Set<PollOptionLegacy>
|
||||
@NSManaged public private(set) var votePolls: Set<PollLegacy>
|
||||
// relationships
|
||||
@NSManaged public private(set) var following: Set<MastodonUser>
|
||||
@NSManaged public private(set) var followingBy: Set<MastodonUser>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class Poll: NSManagedObject {
|
||||
public final class PollLegacy: NSManagedObject {
|
||||
public typealias ID = String
|
||||
|
||||
// sourcery: autoGenerateProperty
|
||||
|
@ -41,20 +41,20 @@ public final class Poll: NSManagedObject {
|
|||
@NSManaged public private(set) var status: Status?
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var options: Set<PollOption>
|
||||
@NSManaged public private(set) var options: Set<PollOptionLegacy>
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
extension PollLegacy {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Poll {
|
||||
let object: Poll = context.insertObject()
|
||||
) -> PollLegacy {
|
||||
let object: PollLegacy = context.insertObject()
|
||||
|
||||
object.configure(property: property)
|
||||
|
||||
|
@ -63,23 +63,23 @@ extension Poll {
|
|||
|
||||
}
|
||||
|
||||
extension Poll: Managed {
|
||||
extension PollLegacy: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Poll.createdAt, ascending: false)]
|
||||
return [NSSortDescriptor(keyPath: \PollLegacy.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
extension PollLegacy {
|
||||
static func predicate(domain: String) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Poll.domain), domain)
|
||||
return NSPredicate(format: "%K == %@", #keyPath(PollLegacy.domain), domain)
|
||||
}
|
||||
|
||||
static func predicate(id: ID) -> NSPredicate {
|
||||
return NSPredicate(format: "%K == %@", #keyPath(Poll.id), id)
|
||||
return NSPredicate(format: "%K == %@", #keyPath(PollLegacy.id), id)
|
||||
}
|
||||
|
||||
static func predicate(ids: [ID]) -> NSPredicate {
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(Poll.id), ids)
|
||||
return NSPredicate(format: "%K IN %@", #keyPath(PollLegacy.id), ids)
|
||||
}
|
||||
|
||||
public static func predicate(domain: String, id: ID) -> NSPredicate {
|
||||
|
@ -205,7 +205,7 @@ extension Poll {
|
|||
//}
|
||||
|
||||
// MARK: - AutoGenerateProperty
|
||||
extension Poll: AutoGenerateProperty {
|
||||
extension PollLegacy: AutoGenerateProperty {
|
||||
// sourcery:inline:Poll.AutoGenerateProperty
|
||||
|
||||
// Generated using Sourcery
|
||||
|
@ -268,7 +268,7 @@ extension Poll: AutoGenerateProperty {
|
|||
}
|
||||
|
||||
// MARK: - AutoUpdatableObject
|
||||
extension Poll: AutoUpdatableObject {
|
||||
extension PollLegacy: AutoUpdatableObject {
|
||||
// sourcery:inline:Poll.AutoUpdatableObject
|
||||
|
||||
// Generated using Sourcery
|
||||
|
@ -308,25 +308,25 @@ extension Poll: AutoUpdatableObject {
|
|||
public func update(voted: Bool, by: MastodonUser) {
|
||||
if voted {
|
||||
if !(votedBy ?? Set()).contains(by) {
|
||||
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
|
||||
mutableSetValue(forKey: #keyPath(PollLegacy.votedBy)).add(by)
|
||||
}
|
||||
} else {
|
||||
if (votedBy ?? Set()).contains(by) {
|
||||
mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
|
||||
mutableSetValue(forKey: #keyPath(PollLegacy.votedBy)).remove(by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func attach(options: [PollOption]) {
|
||||
public func attach(options: [PollOptionLegacy]) {
|
||||
for option in options {
|
||||
guard !self.options.contains(option) else { continue }
|
||||
self.mutableSetValue(forKey: #keyPath(Poll.options)).add(option)
|
||||
self.mutableSetValue(forKey: #keyPath(PollLegacy.options)).add(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Set<PollOption> {
|
||||
func sortedByIndex() -> [PollOption] {
|
||||
public extension Set<PollOptionLegacy> {
|
||||
func sortedByIndex() -> [PollOptionLegacy] {
|
||||
sorted(by: { lhs, rhs in lhs.index < rhs.index })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class PollOption: NSManagedObject {
|
||||
public final class PollOptionLegacy: NSManagedObject {
|
||||
|
||||
// sourcery: autoGenerateProperty
|
||||
@NSManaged public private(set) var index: Int64
|
||||
|
@ -28,21 +28,21 @@ public final class PollOption: NSManagedObject {
|
|||
|
||||
// many-to-one relationship
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var poll: Poll?
|
||||
@NSManaged public private(set) var poll: PollLegacy?
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
|
||||
extension PollOption {
|
||||
extension PollOptionLegacy {
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> PollOption {
|
||||
let object: PollOption = context.insertObject()
|
||||
) -> PollOptionLegacy {
|
||||
let object: PollOptionLegacy = context.insertObject()
|
||||
|
||||
object.configure(property: property)
|
||||
|
||||
|
@ -51,9 +51,9 @@ extension PollOption {
|
|||
|
||||
}
|
||||
|
||||
extension PollOption: Managed {
|
||||
extension PollOptionLegacy: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \PollOption.createdAt, ascending: false)]
|
||||
return [NSSortDescriptor(keyPath: \PollOptionLegacy.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ extension PollOption: Managed {
|
|||
//
|
||||
|
||||
// MARK: - AutoGenerateProperty
|
||||
extension PollOption: AutoGenerateProperty {
|
||||
extension PollOptionLegacy: AutoGenerateProperty {
|
||||
// sourcery:inline:PollOption.AutoGenerateProperty
|
||||
|
||||
// Generated using Sourcery
|
||||
|
@ -126,7 +126,7 @@ extension PollOption: AutoGenerateProperty {
|
|||
public let votesCount: Int64
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
public let poll: Poll?
|
||||
public let poll: PollLegacy?
|
||||
|
||||
public init(
|
||||
index: Int64,
|
||||
|
@ -134,7 +134,7 @@ extension PollOption: AutoGenerateProperty {
|
|||
votesCount: Int64,
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
poll: Poll?
|
||||
poll: PollLegacy?
|
||||
) {
|
||||
self.index = index
|
||||
self.title = title
|
||||
|
@ -164,7 +164,7 @@ extension PollOption: AutoGenerateProperty {
|
|||
}
|
||||
|
||||
// MARK: - AutoUpdatableObject
|
||||
extension PollOption: AutoUpdatableObject {
|
||||
extension PollOptionLegacy: AutoUpdatableObject {
|
||||
// sourcery:inline:PollOption.AutoUpdatableObject
|
||||
|
||||
// Generated using Sourcery
|
||||
|
@ -189,7 +189,7 @@ extension PollOption: AutoUpdatableObject {
|
|||
self.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
public func update(poll: Poll?) {
|
||||
public func update(poll: PollLegacy?) {
|
||||
if self.poll != poll {
|
||||
self.poll = poll
|
||||
}
|
||||
|
@ -199,11 +199,11 @@ extension PollOption: AutoUpdatableObject {
|
|||
public func update(voted: Bool, by: MastodonUser) {
|
||||
if voted {
|
||||
if !(self.votedBy ?? Set()).contains(by) {
|
||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
||||
self.mutableSetValue(forKey: #keyPath(PollOptionLegacy.votedBy)).add(by)
|
||||
}
|
||||
} else {
|
||||
if (self.votedBy ?? Set()).contains(by) {
|
||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
|
||||
self.mutableSetValue(forKey: #keyPath(PollOptionLegacy.votedBy)).remove(by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ public final class Status: NSManagedObject {
|
|||
@NSManaged public private(set) var replyTo: Status?
|
||||
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var poll: Poll?
|
||||
@NSManaged public private(set) var poll: PollLegacy?
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var card: Card?
|
||||
|
||||
|
@ -379,13 +379,13 @@ extension Status: AutoGenerateRelationship {
|
|||
public struct Relationship {
|
||||
public let application: Application?
|
||||
public let reblog: Status?
|
||||
public let poll: Poll?
|
||||
public let poll: PollLegacy?
|
||||
public let card: Card?
|
||||
|
||||
public init(
|
||||
application: Application?,
|
||||
reblog: Status?,
|
||||
poll: Poll?,
|
||||
poll: PollLegacy?,
|
||||
card: Card?
|
||||
) {
|
||||
self.application = application
|
||||
|
|
|
@ -49,6 +49,8 @@ final public class FeedDataController {
|
|||
updateReblogged(status, isReblogged)
|
||||
case let .toggleSensitive(isVisible):
|
||||
updateSensitive(status, isVisible)
|
||||
case .pollVote:
|
||||
updateEdited(status) // technically the data changed so refresh it to reflect the new data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension Poll.Property {
|
||||
extension PollLegacy.Property {
|
||||
public init(
|
||||
entity: Mastodon.Entity.Poll,
|
||||
domain: String,
|
||||
|
|
|
@ -9,9 +9,9 @@ import Foundation
|
|||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
|
||||
extension PollOption.Property {
|
||||
extension PollOptionLegacy.Property {
|
||||
public init(
|
||||
poll: Poll,
|
||||
poll: PollLegacy,
|
||||
index: Int,
|
||||
entity: Mastodon.Entity.Poll.Option,
|
||||
networkDate: Date
|
||||
|
|
|
@ -53,6 +53,8 @@ public final class StatusDataController {
|
|||
updateReblogged(status, isReblogged)
|
||||
case let .toggleSensitive(isVisible):
|
||||
updateSensitive(status, isVisible)
|
||||
case .pollVote:
|
||||
updateEdited(status) // technically the data changed so refresh it to reflect the new data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,6 @@ import CoreDataStack
|
|||
import MastodonSDK
|
||||
|
||||
public enum PollItem: Hashable {
|
||||
case option(record: ManagedObjectRecord<PollOption>)
|
||||
case option(record: MastodonPollOption)
|
||||
case history(option: Mastodon.Entity.StatusEdit.Poll.Option)
|
||||
}
|
||||
|
|
|
@ -31,11 +31,11 @@ extension Persistence.Poll {
|
|||
}
|
||||
|
||||
public struct PersistResult {
|
||||
public let poll: Poll
|
||||
public let poll: PollLegacy
|
||||
public let isNewInsertion: Bool
|
||||
|
||||
public init(
|
||||
poll: Poll,
|
||||
poll: PollLegacy,
|
||||
isNewInsertion: Bool
|
||||
) {
|
||||
self.poll = poll
|
||||
|
@ -74,9 +74,9 @@ extension Persistence.Poll {
|
|||
public static func fetch(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
context: PersistContext
|
||||
) -> Poll? {
|
||||
let request = Poll.sortedFetchRequest
|
||||
request.predicate = Poll.predicate(domain: context.domain, id: context.entity.id)
|
||||
) -> PollLegacy? {
|
||||
let request = PollLegacy.sortedFetchRequest
|
||||
request.predicate = PollLegacy.predicate(domain: context.domain, id: context.entity.id)
|
||||
request.fetchLimit = 1
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
|
@ -90,13 +90,13 @@ extension Persistence.Poll {
|
|||
public static func create(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
context: PersistContext
|
||||
) -> Poll {
|
||||
let property = Poll.Property(
|
||||
) -> PollLegacy {
|
||||
let property = PollLegacy.Property(
|
||||
entity: context.entity,
|
||||
domain: context.domain,
|
||||
networkDate: context.networkDate
|
||||
)
|
||||
let poll = Poll.insert(
|
||||
let poll = PollLegacy.insert(
|
||||
into: managedObjectContext,
|
||||
property: property
|
||||
)
|
||||
|
@ -106,11 +106,11 @@ extension Persistence.Poll {
|
|||
|
||||
public static func merge(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
poll: Poll,
|
||||
poll: PollLegacy,
|
||||
context: PersistContext
|
||||
) {
|
||||
guard context.networkDate > poll.updatedAt else { return }
|
||||
let property = Poll.Property(
|
||||
let property = PollLegacy.Property(
|
||||
entity: context.entity,
|
||||
domain: context.domain,
|
||||
networkDate: context.networkDate
|
||||
|
@ -121,7 +121,7 @@ extension Persistence.Poll {
|
|||
|
||||
public static func update(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
poll: Poll,
|
||||
poll: PollLegacy,
|
||||
context: PersistContext
|
||||
) {
|
||||
let optionEntities = context.entity.options
|
||||
|
@ -159,7 +159,7 @@ extension Persistence.Poll {
|
|||
option.update(poll: nil)
|
||||
managedObjectContext.delete(option)
|
||||
}
|
||||
var attachableOptions = [PollOption]()
|
||||
var attachableOptions = [PollOptionLegacy]()
|
||||
for (index, option) in context.entity.options.enumerated() {
|
||||
attachableOptions.append(
|
||||
Persistence.PollOption.create(
|
||||
|
@ -180,7 +180,7 @@ extension Persistence.Poll {
|
|||
poll.update(updatedAt: context.networkDate)
|
||||
}
|
||||
|
||||
private static func needsPollOptionsUpdate(context: PersistContext, poll: Poll) -> Bool {
|
||||
private static func needsPollOptionsUpdate(context: PersistContext, poll: PollLegacy) -> Bool {
|
||||
let entityPollOptions = context.entity.options.map { (title: $0.title, votes: $0.votesCount) }
|
||||
let pollOptions = poll.options.sortedByIndex().map { (title: $0.title, votes: Int($0.votesCount)) }
|
||||
|
||||
|
|
|
@ -14,14 +14,14 @@ extension Persistence.PollOption {
|
|||
|
||||
public struct PersistContext {
|
||||
public let index: Int
|
||||
public let poll: Poll
|
||||
public let poll: PollLegacy
|
||||
public let entity: Mastodon.Entity.Poll.Option
|
||||
public let me: MastodonUser?
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
index: Int,
|
||||
poll: Poll,
|
||||
poll: PollLegacy,
|
||||
entity: Mastodon.Entity.Poll.Option,
|
||||
me: MastodonUser?,
|
||||
networkDate: Date
|
||||
|
@ -35,11 +35,11 @@ extension Persistence.PollOption {
|
|||
}
|
||||
|
||||
public struct PersistResult {
|
||||
public let option: PollOption
|
||||
public let option: PollOptionLegacy
|
||||
public let isNewInsertion: Bool
|
||||
|
||||
public init(
|
||||
option: PollOption,
|
||||
option: PollOptionLegacy,
|
||||
isNewInsertion: Bool
|
||||
) {
|
||||
self.option = option
|
||||
|
@ -65,24 +65,24 @@ extension Persistence.PollOption {
|
|||
public static func create(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
context: PersistContext
|
||||
) -> PollOption {
|
||||
let property = PollOption.Property(
|
||||
) -> PollOptionLegacy {
|
||||
let property = PollOptionLegacy.Property(
|
||||
poll: context.poll,
|
||||
index: context.index,
|
||||
entity: context.entity,
|
||||
networkDate: context.networkDate
|
||||
)
|
||||
let option = PollOption.insert(into: managedObjectContext, property: property)
|
||||
let option = PollOptionLegacy.insert(into: managedObjectContext, property: property)
|
||||
update(option: option, context: context)
|
||||
return option
|
||||
}
|
||||
|
||||
public static func merge(
|
||||
option: PollOption,
|
||||
option: PollOptionLegacy,
|
||||
context: PersistContext
|
||||
) {
|
||||
guard context.networkDate > option.updatedAt else { return }
|
||||
let property = PollOption.Property(
|
||||
let property = PollOptionLegacy.Property(
|
||||
poll: context.poll,
|
||||
index: context.index,
|
||||
entity: context.entity,
|
||||
|
@ -93,7 +93,7 @@ extension Persistence.PollOption {
|
|||
}
|
||||
|
||||
private static func update(
|
||||
option: PollOption,
|
||||
option: PollOptionLegacy,
|
||||
context: PersistContext
|
||||
) {
|
||||
// Do nothing
|
||||
|
|
|
@ -78,7 +78,7 @@ extension Persistence.Status {
|
|||
isNewInsertion: false
|
||||
)
|
||||
} else {
|
||||
let poll: Poll? = {
|
||||
let poll: PollLegacy? = {
|
||||
guard let entity = context.entity.poll else { return nil }
|
||||
let result = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
|
|
|
@ -42,20 +42,6 @@ extension APIService {
|
|||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
#warning("TODO: Remove this with IOS-181, IOS-182")
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
|
||||
for entity in response.value {
|
||||
guard let poll = entity.poll else { continue }
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -41,20 +41,6 @@ extension APIService {
|
|||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
#warning("TODO: Remove this with IOS-181, IOS-182")
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
|
||||
for entity in response.value {
|
||||
guard let poll = entity.poll else { continue }
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -14,39 +14,18 @@ import MastodonSDK
|
|||
extension APIService {
|
||||
|
||||
public func poll(
|
||||
poll: ManagedObjectRecord<Poll>,
|
||||
poll: Mastodon.Entity.Poll,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
let pollID: Poll.ID = try await managedObjectContext.perform {
|
||||
guard let poll = poll.object(in: managedObjectContext) else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
return poll.id
|
||||
}
|
||||
|
||||
let response = try await Mastodon.API.Polls.poll(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
pollID: pollID,
|
||||
pollID: poll.id,
|
||||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Poll.PersistContext(
|
||||
domain: authenticationBox.domain,
|
||||
entity: response.value,
|
||||
me: me,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
@ -55,41 +34,19 @@ extension APIService {
|
|||
extension APIService {
|
||||
|
||||
public func vote(
|
||||
poll: ManagedObjectRecord<Poll>,
|
||||
poll: Mastodon.Entity.Poll,
|
||||
choices: [Int],
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
let _pollID: Poll.ID? = try await managedObjectContext.perform {
|
||||
guard let poll = poll.object(in: managedObjectContext) else { return nil }
|
||||
return poll.id
|
||||
}
|
||||
|
||||
guard let pollID = _pollID else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
let response = try await Mastodon.API.Polls.vote(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
pollID: pollID,
|
||||
pollID: poll.id,
|
||||
query: Mastodon.API.Polls.VoteQuery(choices: choices),
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.Poll.PersistContext(
|
||||
domain: authenticationBox.domain,
|
||||
entity: response.value,
|
||||
me: me,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -27,20 +27,6 @@ extension APIService {
|
|||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
#warning("TODO: Remove this with IOS-181, IOS-182")
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
|
||||
for entity in response.value {
|
||||
guard let poll = entity.poll else { continue }
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} // end func
|
||||
|
||||
|
|
|
@ -27,19 +27,6 @@ extension APIService {
|
|||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
#warning("TODO: Remove this with IOS-181, IOS-182")
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
|
||||
if let poll = response.value.poll {
|
||||
_ = Persistence.Poll.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/2/24
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/poll/)
|
||||
public struct Poll: Codable, Sendable {
|
||||
public struct Poll: Codable, Sendable, Hashable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
|
@ -47,7 +47,7 @@ extension Mastodon.Entity {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Poll {
|
||||
public struct Option: Codable, Sendable {
|
||||
public struct Option: Codable, Sendable, Hashable {
|
||||
public let title: String
|
||||
/// nil if results are not published yet
|
||||
public let votesCount: Int?
|
||||
|
|
|
@ -71,17 +71,29 @@ extension MastodonFeed: Hashable {
|
|||
public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool {
|
||||
lhs.id == rhs.id &&
|
||||
lhs.status?.entity == rhs.status?.entity &&
|
||||
lhs.status?.poll == rhs.status?.poll &&
|
||||
lhs.status?.reblog?.entity == rhs.status?.reblog?.entity &&
|
||||
lhs.status?.reblog?.poll == rhs.status?.reblog?.poll &&
|
||||
lhs.status?.isSensitiveToggled == rhs.status?.isSensitiveToggled &&
|
||||
lhs.status?.reblog?.isSensitiveToggled == rhs.status?.reblog?.isSensitiveToggled
|
||||
lhs.status?.reblog?.isSensitiveToggled == rhs.status?.reblog?.isSensitiveToggled &&
|
||||
lhs.status?.poll == rhs.status?.poll &&
|
||||
lhs.status?.reblog?.poll == rhs.status?.reblog?.poll &&
|
||||
lhs.status?.poll?.entity == rhs.status?.poll?.entity &&
|
||||
lhs.status?.reblog?.poll?.entity == rhs.status?.reblog?.poll?.entity
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(status?.entity)
|
||||
hasher.combine(status?.poll)
|
||||
hasher.combine(status?.reblog?.entity)
|
||||
hasher.combine(status?.reblog?.poll)
|
||||
hasher.combine(status?.isSensitiveToggled)
|
||||
hasher.combine(status?.reblog?.isSensitiveToggled)
|
||||
hasher.combine(status?.poll)
|
||||
hasher.combine(status?.reblog?.poll)
|
||||
hasher.combine(status?.poll?.entity)
|
||||
hasher.combine(status?.reblog?.poll?.entity)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class MastodonPoll: ObservableObject, Hashable {
|
||||
|
||||
@Published public var votersCount: Int?
|
||||
@Published public var votesCount: Int
|
||||
@Published public var options: [MastodonPollOption] = []
|
||||
@Published public var multiple: Bool
|
||||
@Published public var expired: Bool
|
||||
@Published public var expiresAt: Date?
|
||||
@Published public var voted: Bool?
|
||||
|
||||
public var id: String {
|
||||
entity.id
|
||||
}
|
||||
|
||||
public let entity: Mastodon.Entity.Poll
|
||||
public weak var status: MastodonStatus?
|
||||
|
||||
public init(poll: Mastodon.Entity.Poll, status: MastodonStatus?) {
|
||||
self.status = status
|
||||
self.entity = poll
|
||||
self.votersCount = poll.votersCount
|
||||
self.votesCount = poll.votesCount
|
||||
self.multiple = poll.multiple
|
||||
self.expired = poll.expired
|
||||
self.voted = poll.voted
|
||||
self.expiresAt = poll.expiresAt
|
||||
self.options = poll.options.map { $0.toMastodonPollOption(with: self) }
|
||||
}
|
||||
|
||||
public static func == (lhs: MastodonPoll, rhs: MastodonPoll) -> Bool {
|
||||
lhs.entity == rhs.entity
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(entity)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Mastodon.Entity.Poll {
|
||||
func toMastodonPoll(status: MastodonStatus?) -> MastodonPoll {
|
||||
return .init(poll: self, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
public final class MastodonPollOption: ObservableObject, Hashable {
|
||||
|
||||
public let poll: MastodonPoll
|
||||
public let option: Mastodon.Entity.Poll.Option
|
||||
@Published public var isSelected: Bool = false
|
||||
@Published public var votesCount: Int?
|
||||
@Published public var title: String
|
||||
@Published public var voted: Bool?
|
||||
public private(set) var optionIndex: Int? = nil
|
||||
|
||||
public init(poll: MastodonPoll, option: Mastodon.Entity.Poll.Option, isSelected: Bool = false) {
|
||||
self.poll = poll
|
||||
self.option = option
|
||||
self.isSelected = isSelected
|
||||
self.votesCount = option.votesCount
|
||||
self.title = option.title
|
||||
self.optionIndex = poll.options.firstIndex(of: self)
|
||||
|
||||
self.voted = {
|
||||
guard let ownVotes = poll.entity.ownVotes else { return false }
|
||||
guard let optionIndex else { return false }
|
||||
return ownVotes.contains(optionIndex)
|
||||
}()
|
||||
}
|
||||
|
||||
public static func == (lhs: MastodonPollOption, rhs: MastodonPollOption) -> Bool {
|
||||
lhs.poll == rhs.poll && lhs.option == rhs.option && lhs.isSelected == rhs.isSelected
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(poll)
|
||||
hasher.combine(option)
|
||||
hasher.combine(isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Mastodon.Entity.Poll.Option {
|
||||
func toMastodonPollOption(with poll: MastodonPoll) -> MastodonPollOption {
|
||||
return .init(poll: poll, option: self)
|
||||
}
|
||||
}
|
|
@ -16,10 +16,16 @@ public final class MastodonStatus: ObservableObject {
|
|||
|
||||
@Published public var isSensitiveToggled: Bool = false
|
||||
|
||||
@Published public var poll: MastodonPoll?
|
||||
|
||||
init(entity: Mastodon.Entity.Status, isSensitiveToggled: Bool) {
|
||||
self.entity = entity
|
||||
self.isSensitiveToggled = isSensitiveToggled
|
||||
|
||||
if let poll = entity.poll {
|
||||
self.poll = .init(poll: poll, status: self)
|
||||
}
|
||||
|
||||
if let reblog = entity.reblog {
|
||||
self.reblog = MastodonStatus.fromEntity(reblog)
|
||||
} else {
|
||||
|
@ -47,19 +53,30 @@ extension MastodonStatus {
|
|||
originalStatus = status
|
||||
return self
|
||||
}
|
||||
|
||||
public func withPoll(_ poll: MastodonPoll?) -> MastodonStatus {
|
||||
self.poll = poll
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonStatus: Hashable {
|
||||
public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool {
|
||||
lhs.entity == rhs.entity &&
|
||||
lhs.poll == rhs.poll &&
|
||||
lhs.entity.poll == rhs.entity.poll &&
|
||||
lhs.reblog?.entity == rhs.reblog?.entity &&
|
||||
lhs.reblog?.poll == rhs.reblog?.poll &&
|
||||
lhs.reblog?.entity.poll == rhs.reblog?.entity.poll &&
|
||||
lhs.isSensitiveToggled == rhs.isSensitiveToggled &&
|
||||
lhs.reblog?.isSensitiveToggled == rhs.reblog?.isSensitiveToggled
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(entity)
|
||||
hasher.combine(poll)
|
||||
hasher.combine(reblog?.entity)
|
||||
hasher.combine(reblog?.poll)
|
||||
hasher.combine(isSensitiveToggled)
|
||||
hasher.combine(reblog?.isSensitiveToggled)
|
||||
}
|
||||
|
@ -84,17 +101,16 @@ public extension MastodonStatus {
|
|||
case toggleSensitive(Bool)
|
||||
case delete
|
||||
case edit
|
||||
case pollVote
|
||||
}
|
||||
}
|
||||
|
||||
public extension MastodonStatus {
|
||||
func getPoll(in context: NSManagedObjectContext, domain: String) async -> Poll? {
|
||||
func getPoll(in domain: String, authorization: Mastodon.API.OAuth.Authorization) async -> Mastodon.Entity.Poll? {
|
||||
guard
|
||||
let pollId = entity.poll?.id
|
||||
else { return nil }
|
||||
return try? await context.perform {
|
||||
let predicate = Poll.predicate(domain: domain, id: pollId)
|
||||
return Poll.findOrFetch(in: context, matching: predicate)
|
||||
}
|
||||
let poll = try? await Mastodon.API.Polls.poll(session: .shared, domain: domain, pollID: pollId, authorization: authorization).singleOutput().value
|
||||
return poll
|
||||
}
|
||||
}
|
||||
|
|
|
@ -281,13 +281,16 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
contentWarning = status.entity.spoilerText ?? ""
|
||||
}
|
||||
Task { @MainActor in
|
||||
if let poll = await status.getPoll(in: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) {
|
||||
if let poll = await status.getPoll(
|
||||
in: authContext.mastodonAuthenticationBox.domain,
|
||||
authorization: authContext.mastodonAuthenticationBox.userAuthorization
|
||||
) {
|
||||
isPollActive = !poll.expired
|
||||
pollMultipleConfigurationOption = poll.multiple
|
||||
if let pollExpiresAt = poll.expiresAt {
|
||||
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||
}
|
||||
pollOptions = poll.options.sortedByIndex().map {
|
||||
pollOptions = poll.options.map {
|
||||
let option = PollComposeItem.Option()
|
||||
option.text = $0.title
|
||||
return option
|
||||
|
|
|
@ -378,68 +378,44 @@ extension StatusView {
|
|||
private func configurePoll(status: MastodonStatus) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
guard
|
||||
let context = viewModel.context?.managedObjectContext,
|
||||
let domain = viewModel.authContext?.mastodonAuthenticationBox.domain,
|
||||
let pollId = status.entity.poll?.id
|
||||
else {
|
||||
guard let poll = status.poll else {
|
||||
return
|
||||
}
|
||||
|
||||
let predicate = Poll.predicate(domain: domain, id: pollId)
|
||||
guard let poll = Poll.findOrFetch(in: context, matching: predicate) else { return }
|
||||
|
||||
viewModel.managedObjects.insert(poll)
|
||||
|
||||
// pollItems
|
||||
let options = poll.options.sorted(by: { $0.index < $1.index })
|
||||
let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) }
|
||||
let options = poll.options
|
||||
let items: [PollItem] = options.map { .option(record: $0) }
|
||||
self.viewModel.pollItems = items
|
||||
|
||||
// isVoteButtonEnabled
|
||||
poll.publisher(for: \.updatedAt)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let options = poll.options
|
||||
let hasSelectedOption = options.contains(where: { $0.isSelected })
|
||||
self.viewModel.isVoteButtonEnabled = hasSelectedOption
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// isVotable
|
||||
let hasSelectedOption = options.contains(where: { $0.isSelected == true })
|
||||
viewModel.isVoteButtonEnabled = hasSelectedOption
|
||||
|
||||
Publishers.CombineLatest(
|
||||
poll.publisher(for: \.votedBy),
|
||||
poll.publisher(for: \.expired)
|
||||
poll.$voted,
|
||||
poll.$expired
|
||||
)
|
||||
.map { [weak viewModel] votedBy, expired in
|
||||
guard let viewModel = viewModel else { return false }
|
||||
guard let authContext = viewModel.authContext else { return false }
|
||||
let domain = authContext.mastodonAuthenticationBox.domain
|
||||
let userID = authContext.mastodonAuthenticationBox.userID
|
||||
let isVoted = votedBy?.contains(where: { $0.domain == domain && $0.id == userID }) ?? false
|
||||
return !isVoted && !expired
|
||||
.map { voted, expired in
|
||||
return voted == false && expired == false
|
||||
}
|
||||
.assign(to: &viewModel.$isVotable)
|
||||
|
||||
// votesCount
|
||||
poll.publisher(for: \.votesCount)
|
||||
.map { Int($0) }
|
||||
poll.$votesCount
|
||||
.assign(to: \.voteCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// voterCount
|
||||
poll.publisher(for: \.votersCount)
|
||||
.map { Int($0) }
|
||||
|
||||
poll.$votersCount
|
||||
.assign(to: \.voterCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expireAt
|
||||
poll.publisher(for: \.expiresAt)
|
||||
|
||||
poll.$expiresAt
|
||||
.assign(to: \.expireAt, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// expired
|
||||
poll.publisher(for: \.expired)
|
||||
|
||||
poll.$expired
|
||||
.assign(to: \.expired, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isVoting
|
||||
poll.publisher(for: \.isVoting)
|
||||
|
||||
poll.$voted
|
||||
.map { $0 == true }
|
||||
.assign(to: \.isVoting, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
|
|
@ -457,8 +457,32 @@ extension StatusView.ViewModel {
|
|||
|
||||
statusView.pollTableViewHeightLayoutConstraint.constant = CGFloat(items.count) * PollOptionTableViewCell.height
|
||||
statusView.setPollDisplay()
|
||||
|
||||
items.forEach({ item in
|
||||
guard case let PollItem.option(record) = item else { return }
|
||||
record.$isSelected.receive(on: DispatchQueue.main).sink { [weak self] selected in
|
||||
guard let self else { return }
|
||||
if (selected) {
|
||||
// as we have just selected an option, the vote button must be enabled
|
||||
self.isVoteButtonEnabled = true
|
||||
} else {
|
||||
// figure out which buttons are currently selected
|
||||
let records = pollItems.compactMap({ item -> MastodonPollOption? in
|
||||
guard case let PollItem.option(record) = item else { return nil }
|
||||
return record
|
||||
})
|
||||
.filter({ $0.isSelected })
|
||||
|
||||
// only enable vote button if there are selected options
|
||||
self.isVoteButtonEnabled = !records.isEmpty
|
||||
}
|
||||
statusView.pollTableView.reloadData()
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
})
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$isVotable
|
||||
.sink { isVotable in
|
||||
statusView.pollTableView.allowsSelection = isVotable
|
||||
|
@ -508,14 +532,17 @@ extension StatusView.ViewModel {
|
|||
$isVotable,
|
||||
$isVoting
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { isVotable, isVoting in
|
||||
guard isVotable else {
|
||||
statusView.pollVoteButton.isHidden = true
|
||||
statusView.pollVoteActivityIndicatorView.isHidden = true
|
||||
statusView.pollTableView.isUserInteractionEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
statusView.pollVoteButton.isHidden = isVoting
|
||||
statusView.pollTableView.isUserInteractionEnabled = !isVoting
|
||||
statusView.pollVoteActivityIndicatorView.isHidden = !isVoting
|
||||
statusView.pollVoteActivityIndicatorView.startAnimating()
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import Meta
|
|||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
public extension CGSize {
|
||||
static let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
||||
|
|
Loading…
Reference in New Issue