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
|
cell.pollOptionView.viewModel.authContext = authContext
|
||||||
|
|
||||||
managedObjectContext.performAndWait {
|
cell.pollOptionView.configure(pollOption: record)
|
||||||
guard let option = record.object(in: managedObjectContext) else {
|
|
||||||
assertionFailure()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,3 +523,70 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||||
} // end Task
|
} // 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 MastodonLocalization
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
// MARK: - header
|
// MARK: - header
|
||||||
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
|
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 pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
|
||||||
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
let managedObjectContext = context.managedObjectContext
|
|
||||||
|
|
||||||
Task {
|
|
||||||
guard case let .option(pollOption) = pollItem else {
|
guard case let .option(pollOption) = pollItem else {
|
||||||
assertionFailure("only works for status data provider")
|
assertionFailure("only works for status data provider")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var _poll: ManagedObjectRecord<Poll>?
|
let poll = pollOption.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 }
|
|
||||||
|
|
||||||
if !poll.multiple {
|
if !poll.multiple {
|
||||||
for option in poll.options where option != pollOption {
|
poll.options.forEach { $0.isSelected = false }
|
||||||
option.update(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(
|
func tableViewCell(
|
||||||
|
@ -334,45 +289,30 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||||
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
|
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
|
||||||
guard case let .option(firstPollOption) = firstPollItem else { return }
|
guard case let .option(firstPollOption) = firstPollItem else { return }
|
||||||
|
|
||||||
let managedObjectContext = context.managedObjectContext
|
statusView.viewModel.isVoting = true
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
var _poll: ManagedObjectRecord<Poll>?
|
let poll = firstPollOption.poll
|
||||||
var _choices: [Int]?
|
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
let choices = poll.options
|
||||||
guard let poll = firstPollOption.object(in: managedObjectContext)?.poll else { return }
|
.filter { $0.isSelected == true }
|
||||||
_poll = .init(objectID: poll.objectID)
|
.compactMap { poll.options.firstIndex(of: $0) }
|
||||||
|
|
||||||
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 }
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try await context.apiService.vote(
|
let newPoll = try await context.apiService.vote(
|
||||||
poll: poll,
|
poll: poll.entity,
|
||||||
choices: choices,
|
choices: choices,
|
||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
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 {
|
} catch {
|
||||||
// restore voting state
|
statusView.viewModel.isVoting = false
|
||||||
try await managedObjectContext.performChanges {
|
|
||||||
guard let poll = poll.object(in: managedObjectContext) else { return }
|
|
||||||
poll.update(isVoting: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end Task
|
} // 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, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
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, 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, 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, authorAvatarButtonDidPressed button: AvatarButton)
|
||||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
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)
|
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) {
|
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)
|
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, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
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, 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, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||||
|
|
||||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
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) {
|
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) {
|
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) {
|
public func statusView(_ statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||||
|
|
|
@ -14,92 +14,55 @@ import MastodonUI
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension PollOptionView {
|
extension PollOptionView {
|
||||||
public func configure(pollOption option: PollOption, status: MastodonStatus?) {
|
public func configure(pollOption option: MastodonPollOption) {
|
||||||
guard let poll = option.poll else {
|
let poll = option.poll
|
||||||
assertionFailure("PollOption to be configured is expected to be part of Poll with Status")
|
let status = option.poll.status
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.objects.insert(option)
|
|
||||||
|
|
||||||
// metaContent
|
// metaContent
|
||||||
option.publisher(for: \.title)
|
option.$title
|
||||||
.map { title -> MetaContent? in
|
.map { title -> MetaContent? in
|
||||||
return PlaintextMetaContent(string: title)
|
return PlaintextMetaContent(string: title)
|
||||||
}
|
}
|
||||||
.assign(to: \.metaContent, on: viewModel)
|
.assign(to: \.metaContent, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// percentage
|
// percentage
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
poll.publisher(for: \.votersCount),
|
poll.$votersCount,
|
||||||
option.publisher(for: \.votesCount)
|
option.$votesCount
|
||||||
)
|
)
|
||||||
.map { pollVotersCount, optionVotesCount -> Double? in
|
.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)
|
return Double(optionVotesCount) / Double(pollVotersCount)
|
||||||
}
|
}
|
||||||
.assign(to: \.percentage, on: viewModel)
|
.assign(to: \.percentage, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// $isExpire
|
// $isExpire
|
||||||
poll.publisher(for: \.expired)
|
poll.$expired
|
||||||
.assign(to: \.isExpire, on: viewModel)
|
.assign(to: \.isExpire, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// isMultiple
|
// isMultiple
|
||||||
viewModel.isMultiple = poll.multiple
|
viewModel.isMultiple = poll.multiple
|
||||||
|
|
||||||
let optionIndex = option.index
|
let authContext = viewModel.authContext
|
||||||
|
|
||||||
let authorDomain = status?.entity.account.domain ?? ""
|
let authorDomain = status?.entity.account.domain ?? ""
|
||||||
let authorID = status?.entity.account.id ?? ""
|
let authorID = status?.entity.account.id ?? ""
|
||||||
// isSelect, isPollVoted, isMyPoll
|
// 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 domain = authContext?.mastodonAuthenticationBox.domain ?? ""
|
||||||
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
||||||
|
|
||||||
let options = poll.options
|
|
||||||
let pollVoteBy = poll.votedBy ?? Set()
|
|
||||||
|
|
||||||
let isMyPoll = authorDomain == domain
|
let isMyPoll = authorDomain == domain
|
||||||
&& authorID == userID
|
&& authorID == userID
|
||||||
|
|
||||||
let votedOptions = options.filter { option in
|
self.viewModel.isSelect = option.isSelected
|
||||||
let votedBy = option.votedBy ?? Set()
|
self.viewModel.isPollVoted = poll.voted == true
|
||||||
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.isMyPoll = isMyPoll
|
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"
|
// show left-hand-side dots, otherwise view looks "incomplete"
|
||||||
viewModel.selectState = .off
|
viewModel.selectState = .off
|
||||||
// appearance
|
// appearance
|
||||||
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
|
checkmarkBackgroundView.backgroundColor = SystemTheme.tableViewCellSelectionBackgroundColor
|
||||||
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
// sourcery: protocolName = "StatusViewDelegate"
|
// sourcery: protocolName = "StatusViewDelegate"
|
||||||
// sourcery: replaceOf = "statusView(statusView"
|
// sourcery: replaceOf = "statusView(statusView"
|
||||||
|
|
|
@ -86,6 +86,8 @@ extension ThreadViewController: DataSourceProvider {
|
||||||
viewModel.handleEdit(status)
|
viewModel.handleEdit(status)
|
||||||
case .delete:
|
case .delete:
|
||||||
break // this case has already been handled
|
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 reblogged: Set<Status>
|
||||||
@NSManaged public private(set) var muted: Set<Status>
|
@NSManaged public private(set) var muted: Set<Status>
|
||||||
@NSManaged public private(set) var bookmarked: Set<Status>
|
@NSManaged public private(set) var bookmarked: Set<Status>
|
||||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>
|
@NSManaged public private(set) var votePollOptions: Set<PollOptionLegacy>
|
||||||
@NSManaged public private(set) var votePolls: Set<Poll>
|
@NSManaged public private(set) var votePolls: Set<PollLegacy>
|
||||||
// relationships
|
// relationships
|
||||||
@NSManaged public private(set) var following: Set<MastodonUser>
|
@NSManaged public private(set) var following: Set<MastodonUser>
|
||||||
@NSManaged public private(set) var followingBy: Set<MastodonUser>
|
@NSManaged public private(set) var followingBy: Set<MastodonUser>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
public final class Poll: NSManagedObject {
|
public final class PollLegacy: NSManagedObject {
|
||||||
public typealias ID = String
|
public typealias ID = String
|
||||||
|
|
||||||
// sourcery: autoGenerateProperty
|
// sourcery: autoGenerateProperty
|
||||||
|
@ -41,20 +41,20 @@ public final class Poll: NSManagedObject {
|
||||||
@NSManaged public private(set) var status: Status?
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var options: Set<PollOption>
|
@NSManaged public private(set) var options: Set<PollOptionLegacy>
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Poll {
|
extension PollLegacy {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public static func insert(
|
public static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property
|
property: Property
|
||||||
) -> Poll {
|
) -> PollLegacy {
|
||||||
let object: Poll = context.insertObject()
|
let object: PollLegacy = context.insertObject()
|
||||||
|
|
||||||
object.configure(property: property)
|
object.configure(property: property)
|
||||||
|
|
||||||
|
@ -63,23 +63,23 @@ extension Poll {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Poll: Managed {
|
extension PollLegacy: Managed {
|
||||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
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 {
|
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 {
|
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 {
|
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 {
|
public static func predicate(domain: String, id: ID) -> NSPredicate {
|
||||||
|
@ -205,7 +205,7 @@ extension Poll {
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// MARK: - AutoGenerateProperty
|
// MARK: - AutoGenerateProperty
|
||||||
extension Poll: AutoGenerateProperty {
|
extension PollLegacy: AutoGenerateProperty {
|
||||||
// sourcery:inline:Poll.AutoGenerateProperty
|
// sourcery:inline:Poll.AutoGenerateProperty
|
||||||
|
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
|
@ -268,7 +268,7 @@ extension Poll: AutoGenerateProperty {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AutoUpdatableObject
|
// MARK: - AutoUpdatableObject
|
||||||
extension Poll: AutoUpdatableObject {
|
extension PollLegacy: AutoUpdatableObject {
|
||||||
// sourcery:inline:Poll.AutoUpdatableObject
|
// sourcery:inline:Poll.AutoUpdatableObject
|
||||||
|
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
|
@ -308,25 +308,25 @@ extension Poll: AutoUpdatableObject {
|
||||||
public func update(voted: Bool, by: MastodonUser) {
|
public func update(voted: Bool, by: MastodonUser) {
|
||||||
if voted {
|
if voted {
|
||||||
if !(votedBy ?? Set()).contains(by) {
|
if !(votedBy ?? Set()).contains(by) {
|
||||||
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
|
mutableSetValue(forKey: #keyPath(PollLegacy.votedBy)).add(by)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (votedBy ?? Set()).contains(by) {
|
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 {
|
for option in options {
|
||||||
guard !self.options.contains(option) else { continue }
|
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> {
|
public extension Set<PollOptionLegacy> {
|
||||||
func sortedByIndex() -> [PollOption] {
|
func sortedByIndex() -> [PollOptionLegacy] {
|
||||||
sorted(by: { lhs, rhs in lhs.index < rhs.index })
|
sorted(by: { lhs, rhs in lhs.index < rhs.index })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
public final class PollOption: NSManagedObject {
|
public final class PollOptionLegacy: NSManagedObject {
|
||||||
|
|
||||||
// sourcery: autoGenerateProperty
|
// sourcery: autoGenerateProperty
|
||||||
@NSManaged public private(set) var index: Int64
|
@NSManaged public private(set) var index: Int64
|
||||||
|
@ -28,21 +28,21 @@ public final class PollOption: NSManagedObject {
|
||||||
|
|
||||||
// many-to-one relationship
|
// many-to-one relationship
|
||||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
@NSManaged public private(set) var poll: Poll?
|
@NSManaged public private(set) var poll: PollLegacy?
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension PollOption {
|
extension PollOptionLegacy {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public static func insert(
|
public static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property
|
property: Property
|
||||||
) -> PollOption {
|
) -> PollOptionLegacy {
|
||||||
let object: PollOption = context.insertObject()
|
let object: PollOptionLegacy = context.insertObject()
|
||||||
|
|
||||||
object.configure(property: property)
|
object.configure(property: property)
|
||||||
|
|
||||||
|
@ -51,9 +51,9 @@ extension PollOption {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PollOption: Managed {
|
extension PollOptionLegacy: Managed {
|
||||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
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
|
// MARK: - AutoGenerateProperty
|
||||||
extension PollOption: AutoGenerateProperty {
|
extension PollOptionLegacy: AutoGenerateProperty {
|
||||||
// sourcery:inline:PollOption.AutoGenerateProperty
|
// sourcery:inline:PollOption.AutoGenerateProperty
|
||||||
|
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
|
@ -126,7 +126,7 @@ extension PollOption: AutoGenerateProperty {
|
||||||
public let votesCount: Int64
|
public let votesCount: Int64
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let updatedAt: Date
|
public let updatedAt: Date
|
||||||
public let poll: Poll?
|
public let poll: PollLegacy?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
index: Int64,
|
index: Int64,
|
||||||
|
@ -134,7 +134,7 @@ extension PollOption: AutoGenerateProperty {
|
||||||
votesCount: Int64,
|
votesCount: Int64,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
updatedAt: Date,
|
updatedAt: Date,
|
||||||
poll: Poll?
|
poll: PollLegacy?
|
||||||
) {
|
) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -164,7 +164,7 @@ extension PollOption: AutoGenerateProperty {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AutoUpdatableObject
|
// MARK: - AutoUpdatableObject
|
||||||
extension PollOption: AutoUpdatableObject {
|
extension PollOptionLegacy: AutoUpdatableObject {
|
||||||
// sourcery:inline:PollOption.AutoUpdatableObject
|
// sourcery:inline:PollOption.AutoUpdatableObject
|
||||||
|
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
|
@ -189,7 +189,7 @@ extension PollOption: AutoUpdatableObject {
|
||||||
self.isSelected = isSelected
|
self.isSelected = isSelected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public func update(poll: Poll?) {
|
public func update(poll: PollLegacy?) {
|
||||||
if self.poll != poll {
|
if self.poll != poll {
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
}
|
}
|
||||||
|
@ -199,11 +199,11 @@ extension PollOption: AutoUpdatableObject {
|
||||||
public func update(voted: Bool, by: MastodonUser) {
|
public func update(voted: Bool, by: MastodonUser) {
|
||||||
if voted {
|
if voted {
|
||||||
if !(self.votedBy ?? Set()).contains(by) {
|
if !(self.votedBy ?? Set()).contains(by) {
|
||||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
self.mutableSetValue(forKey: #keyPath(PollOptionLegacy.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(PollOptionLegacy.votedBy)).remove(by)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ public final class Status: NSManagedObject {
|
||||||
@NSManaged public private(set) var replyTo: Status?
|
@NSManaged public private(set) var replyTo: Status?
|
||||||
|
|
||||||
// sourcery: autoGenerateRelationship
|
// sourcery: autoGenerateRelationship
|
||||||
@NSManaged public private(set) var poll: Poll?
|
@NSManaged public private(set) var poll: PollLegacy?
|
||||||
// sourcery: autoGenerateRelationship
|
// sourcery: autoGenerateRelationship
|
||||||
@NSManaged public private(set) var card: Card?
|
@NSManaged public private(set) var card: Card?
|
||||||
|
|
||||||
|
@ -379,13 +379,13 @@ extension Status: AutoGenerateRelationship {
|
||||||
public struct Relationship {
|
public struct Relationship {
|
||||||
public let application: Application?
|
public let application: Application?
|
||||||
public let reblog: Status?
|
public let reblog: Status?
|
||||||
public let poll: Poll?
|
public let poll: PollLegacy?
|
||||||
public let card: Card?
|
public let card: Card?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
application: Application?,
|
application: Application?,
|
||||||
reblog: Status?,
|
reblog: Status?,
|
||||||
poll: Poll?,
|
poll: PollLegacy?,
|
||||||
card: Card?
|
card: Card?
|
||||||
) {
|
) {
|
||||||
self.application = application
|
self.application = application
|
||||||
|
|
|
@ -49,6 +49,8 @@ final public class FeedDataController {
|
||||||
updateReblogged(status, isReblogged)
|
updateReblogged(status, isReblogged)
|
||||||
case let .toggleSensitive(isVisible):
|
case let .toggleSensitive(isVisible):
|
||||||
updateSensitive(status, 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 CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension Poll.Property {
|
extension PollLegacy.Property {
|
||||||
public init(
|
public init(
|
||||||
entity: Mastodon.Entity.Poll,
|
entity: Mastodon.Entity.Poll,
|
||||||
domain: String,
|
domain: String,
|
||||||
|
|
|
@ -9,9 +9,9 @@ import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
|
||||||
extension PollOption.Property {
|
extension PollOptionLegacy.Property {
|
||||||
public init(
|
public init(
|
||||||
poll: Poll,
|
poll: PollLegacy,
|
||||||
index: Int,
|
index: Int,
|
||||||
entity: Mastodon.Entity.Poll.Option,
|
entity: Mastodon.Entity.Poll.Option,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
|
|
|
@ -53,6 +53,8 @@ public final class StatusDataController {
|
||||||
updateReblogged(status, isReblogged)
|
updateReblogged(status, isReblogged)
|
||||||
case let .toggleSensitive(isVisible):
|
case let .toggleSensitive(isVisible):
|
||||||
updateSensitive(status, 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
|
import MastodonSDK
|
||||||
|
|
||||||
public enum PollItem: Hashable {
|
public enum PollItem: Hashable {
|
||||||
case option(record: ManagedObjectRecord<PollOption>)
|
case option(record: MastodonPollOption)
|
||||||
case history(option: Mastodon.Entity.StatusEdit.Poll.Option)
|
case history(option: Mastodon.Entity.StatusEdit.Poll.Option)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,11 @@ extension Persistence.Poll {
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PersistResult {
|
public struct PersistResult {
|
||||||
public let poll: Poll
|
public let poll: PollLegacy
|
||||||
public let isNewInsertion: Bool
|
public let isNewInsertion: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
poll: Poll,
|
poll: PollLegacy,
|
||||||
isNewInsertion: Bool
|
isNewInsertion: Bool
|
||||||
) {
|
) {
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
|
@ -74,9 +74,9 @@ extension Persistence.Poll {
|
||||||
public static func fetch(
|
public static func fetch(
|
||||||
in managedObjectContext: NSManagedObjectContext,
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) -> Poll? {
|
) -> PollLegacy? {
|
||||||
let request = Poll.sortedFetchRequest
|
let request = PollLegacy.sortedFetchRequest
|
||||||
request.predicate = Poll.predicate(domain: context.domain, id: context.entity.id)
|
request.predicate = PollLegacy.predicate(domain: context.domain, id: context.entity.id)
|
||||||
request.fetchLimit = 1
|
request.fetchLimit = 1
|
||||||
do {
|
do {
|
||||||
return try managedObjectContext.fetch(request).first
|
return try managedObjectContext.fetch(request).first
|
||||||
|
@ -90,13 +90,13 @@ extension Persistence.Poll {
|
||||||
public static func create(
|
public static func create(
|
||||||
in managedObjectContext: NSManagedObjectContext,
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) -> Poll {
|
) -> PollLegacy {
|
||||||
let property = Poll.Property(
|
let property = PollLegacy.Property(
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
domain: context.domain,
|
domain: context.domain,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
)
|
)
|
||||||
let poll = Poll.insert(
|
let poll = PollLegacy.insert(
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
property: property
|
property: property
|
||||||
)
|
)
|
||||||
|
@ -106,11 +106,11 @@ extension Persistence.Poll {
|
||||||
|
|
||||||
public static func merge(
|
public static func merge(
|
||||||
in managedObjectContext: NSManagedObjectContext,
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
poll: Poll,
|
poll: PollLegacy,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
guard context.networkDate > poll.updatedAt else { return }
|
guard context.networkDate > poll.updatedAt else { return }
|
||||||
let property = Poll.Property(
|
let property = PollLegacy.Property(
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
domain: context.domain,
|
domain: context.domain,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
|
@ -121,7 +121,7 @@ extension Persistence.Poll {
|
||||||
|
|
||||||
public static func update(
|
public static func update(
|
||||||
in managedObjectContext: NSManagedObjectContext,
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
poll: Poll,
|
poll: PollLegacy,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
let optionEntities = context.entity.options
|
let optionEntities = context.entity.options
|
||||||
|
@ -159,7 +159,7 @@ extension Persistence.Poll {
|
||||||
option.update(poll: nil)
|
option.update(poll: nil)
|
||||||
managedObjectContext.delete(option)
|
managedObjectContext.delete(option)
|
||||||
}
|
}
|
||||||
var attachableOptions = [PollOption]()
|
var attachableOptions = [PollOptionLegacy]()
|
||||||
for (index, option) in context.entity.options.enumerated() {
|
for (index, option) in context.entity.options.enumerated() {
|
||||||
attachableOptions.append(
|
attachableOptions.append(
|
||||||
Persistence.PollOption.create(
|
Persistence.PollOption.create(
|
||||||
|
@ -180,7 +180,7 @@ extension Persistence.Poll {
|
||||||
poll.update(updatedAt: context.networkDate)
|
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 entityPollOptions = context.entity.options.map { (title: $0.title, votes: $0.votesCount) }
|
||||||
let pollOptions = poll.options.sortedByIndex().map { (title: $0.title, votes: Int($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 struct PersistContext {
|
||||||
public let index: Int
|
public let index: Int
|
||||||
public let poll: Poll
|
public let poll: PollLegacy
|
||||||
public let entity: Mastodon.Entity.Poll.Option
|
public let entity: Mastodon.Entity.Poll.Option
|
||||||
public let me: MastodonUser?
|
public let me: MastodonUser?
|
||||||
public let networkDate: Date
|
public let networkDate: Date
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
index: Int,
|
index: Int,
|
||||||
poll: Poll,
|
poll: PollLegacy,
|
||||||
entity: Mastodon.Entity.Poll.Option,
|
entity: Mastodon.Entity.Poll.Option,
|
||||||
me: MastodonUser?,
|
me: MastodonUser?,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
|
@ -35,11 +35,11 @@ extension Persistence.PollOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PersistResult {
|
public struct PersistResult {
|
||||||
public let option: PollOption
|
public let option: PollOptionLegacy
|
||||||
public let isNewInsertion: Bool
|
public let isNewInsertion: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
option: PollOption,
|
option: PollOptionLegacy,
|
||||||
isNewInsertion: Bool
|
isNewInsertion: Bool
|
||||||
) {
|
) {
|
||||||
self.option = option
|
self.option = option
|
||||||
|
@ -65,24 +65,24 @@ extension Persistence.PollOption {
|
||||||
public static func create(
|
public static func create(
|
||||||
in managedObjectContext: NSManagedObjectContext,
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) -> PollOption {
|
) -> PollOptionLegacy {
|
||||||
let property = PollOption.Property(
|
let property = PollOptionLegacy.Property(
|
||||||
poll: context.poll,
|
poll: context.poll,
|
||||||
index: context.index,
|
index: context.index,
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
)
|
)
|
||||||
let option = PollOption.insert(into: managedObjectContext, property: property)
|
let option = PollOptionLegacy.insert(into: managedObjectContext, property: property)
|
||||||
update(option: option, context: context)
|
update(option: option, context: context)
|
||||||
return option
|
return option
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func merge(
|
public static func merge(
|
||||||
option: PollOption,
|
option: PollOptionLegacy,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
guard context.networkDate > option.updatedAt else { return }
|
guard context.networkDate > option.updatedAt else { return }
|
||||||
let property = PollOption.Property(
|
let property = PollOptionLegacy.Property(
|
||||||
poll: context.poll,
|
poll: context.poll,
|
||||||
index: context.index,
|
index: context.index,
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
|
@ -93,7 +93,7 @@ extension Persistence.PollOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func update(
|
private static func update(
|
||||||
option: PollOption,
|
option: PollOptionLegacy,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
|
|
@ -78,7 +78,7 @@ extension Persistence.Status {
|
||||||
isNewInsertion: false
|
isNewInsertion: false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let poll: Poll? = {
|
let poll: PollLegacy? = {
|
||||||
guard let entity = context.entity.poll else { return nil }
|
guard let entity = context.entity.poll else { return nil }
|
||||||
let result = Persistence.Poll.createOrMerge(
|
let result = Persistence.Poll.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
|
|
|
@ -42,20 +42,6 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,20 +41,6 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,39 +14,18 @@ import MastodonSDK
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
public func poll(
|
public func poll(
|
||||||
poll: ManagedObjectRecord<Poll>,
|
poll: Mastodon.Entity.Poll,
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
|
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
|
||||||
let authorization = authenticationBox.userAuthorization
|
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(
|
let response = try await Mastodon.API.Polls.poll(
|
||||||
session: session,
|
session: session,
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
pollID: pollID,
|
pollID: poll.id,
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,41 +34,19 @@ extension APIService {
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
public func vote(
|
public func vote(
|
||||||
poll: ManagedObjectRecord<Poll>,
|
poll: Mastodon.Entity.Poll,
|
||||||
choices: [Int],
|
choices: [Int],
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
|
) 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(
|
let response = try await Mastodon.API.Polls.vote(
|
||||||
session: session,
|
session: session,
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
pollID: pollID,
|
pollID: poll.id,
|
||||||
query: Mastodon.API.Polls.VoteQuery(choices: choices),
|
query: Mastodon.API.Polls.VoteQuery(choices: choices),
|
||||||
authorization: authenticationBox.userAuthorization
|
authorization: authenticationBox.userAuthorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,20 +27,6 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
} // end func
|
} // end func
|
||||||
|
|
||||||
|
|
|
@ -27,19 +27,6 @@ extension APIService {
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
).singleOutput()
|
).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
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
||||||
/// 2021/2/24
|
/// 2021/2/24
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/poll/)
|
/// [Document](https://docs.joinmastodon.org/entities/poll/)
|
||||||
public struct Poll: Codable, Sendable {
|
public struct Poll: Codable, Sendable, Hashable {
|
||||||
public typealias ID = String
|
public typealias ID = String
|
||||||
|
|
||||||
public let id: ID
|
public let id: ID
|
||||||
|
@ -47,7 +47,7 @@ extension Mastodon.Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.Entity.Poll {
|
extension Mastodon.Entity.Poll {
|
||||||
public struct Option: Codable, Sendable {
|
public struct Option: Codable, Sendable, Hashable {
|
||||||
public let title: String
|
public let title: String
|
||||||
/// nil if results are not published yet
|
/// nil if results are not published yet
|
||||||
public let votesCount: Int?
|
public let votesCount: Int?
|
||||||
|
|
|
@ -71,17 +71,29 @@ extension MastodonFeed: Hashable {
|
||||||
public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool {
|
public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool {
|
||||||
lhs.id == rhs.id &&
|
lhs.id == rhs.id &&
|
||||||
lhs.status?.entity == rhs.status?.entity &&
|
lhs.status?.entity == rhs.status?.entity &&
|
||||||
|
lhs.status?.poll == rhs.status?.poll &&
|
||||||
lhs.status?.reblog?.entity == rhs.status?.reblog?.entity &&
|
lhs.status?.reblog?.entity == rhs.status?.reblog?.entity &&
|
||||||
|
lhs.status?.reblog?.poll == rhs.status?.reblog?.poll &&
|
||||||
lhs.status?.isSensitiveToggled == rhs.status?.isSensitiveToggled &&
|
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) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
hasher.combine(status?.entity)
|
hasher.combine(status?.entity)
|
||||||
|
hasher.combine(status?.poll)
|
||||||
hasher.combine(status?.reblog?.entity)
|
hasher.combine(status?.reblog?.entity)
|
||||||
|
hasher.combine(status?.reblog?.poll)
|
||||||
hasher.combine(status?.isSensitiveToggled)
|
hasher.combine(status?.isSensitiveToggled)
|
||||||
hasher.combine(status?.reblog?.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 isSensitiveToggled: Bool = false
|
||||||
|
|
||||||
|
@Published public var poll: MastodonPoll?
|
||||||
|
|
||||||
init(entity: Mastodon.Entity.Status, isSensitiveToggled: Bool) {
|
init(entity: Mastodon.Entity.Status, isSensitiveToggled: Bool) {
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
self.isSensitiveToggled = isSensitiveToggled
|
self.isSensitiveToggled = isSensitiveToggled
|
||||||
|
|
||||||
|
if let poll = entity.poll {
|
||||||
|
self.poll = .init(poll: poll, status: self)
|
||||||
|
}
|
||||||
|
|
||||||
if let reblog = entity.reblog {
|
if let reblog = entity.reblog {
|
||||||
self.reblog = MastodonStatus.fromEntity(reblog)
|
self.reblog = MastodonStatus.fromEntity(reblog)
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,19 +53,30 @@ extension MastodonStatus {
|
||||||
originalStatus = status
|
originalStatus = status
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func withPoll(_ poll: MastodonPoll?) -> MastodonStatus {
|
||||||
|
self.poll = poll
|
||||||
|
return self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonStatus: Hashable {
|
extension MastodonStatus: Hashable {
|
||||||
public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool {
|
public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool {
|
||||||
lhs.entity == rhs.entity &&
|
lhs.entity == rhs.entity &&
|
||||||
|
lhs.poll == rhs.poll &&
|
||||||
|
lhs.entity.poll == rhs.entity.poll &&
|
||||||
lhs.reblog?.entity == rhs.reblog?.entity &&
|
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.isSensitiveToggled == rhs.isSensitiveToggled &&
|
||||||
lhs.reblog?.isSensitiveToggled == rhs.reblog?.isSensitiveToggled
|
lhs.reblog?.isSensitiveToggled == rhs.reblog?.isSensitiveToggled
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(entity)
|
hasher.combine(entity)
|
||||||
|
hasher.combine(poll)
|
||||||
hasher.combine(reblog?.entity)
|
hasher.combine(reblog?.entity)
|
||||||
|
hasher.combine(reblog?.poll)
|
||||||
hasher.combine(isSensitiveToggled)
|
hasher.combine(isSensitiveToggled)
|
||||||
hasher.combine(reblog?.isSensitiveToggled)
|
hasher.combine(reblog?.isSensitiveToggled)
|
||||||
}
|
}
|
||||||
|
@ -84,17 +101,16 @@ public extension MastodonStatus {
|
||||||
case toggleSensitive(Bool)
|
case toggleSensitive(Bool)
|
||||||
case delete
|
case delete
|
||||||
case edit
|
case edit
|
||||||
|
case pollVote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension MastodonStatus {
|
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
|
guard
|
||||||
let pollId = entity.poll?.id
|
let pollId = entity.poll?.id
|
||||||
else { return nil }
|
else { return nil }
|
||||||
return try? await context.perform {
|
let poll = try? await Mastodon.API.Polls.poll(session: .shared, domain: domain, pollID: pollId, authorization: authorization).singleOutput().value
|
||||||
let predicate = Poll.predicate(domain: domain, id: pollId)
|
return poll
|
||||||
return Poll.findOrFetch(in: context, matching: predicate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,13 +281,16 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
contentWarning = status.entity.spoilerText ?? ""
|
contentWarning = status.entity.spoilerText ?? ""
|
||||||
}
|
}
|
||||||
Task { @MainActor in
|
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
|
isPollActive = !poll.expired
|
||||||
pollMultipleConfigurationOption = poll.multiple
|
pollMultipleConfigurationOption = poll.multiple
|
||||||
if let pollExpiresAt = poll.expiresAt {
|
if let pollExpiresAt = poll.expiresAt {
|
||||||
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||||
}
|
}
|
||||||
pollOptions = poll.options.sortedByIndex().map {
|
pollOptions = poll.options.map {
|
||||||
let option = PollComposeItem.Option()
|
let option = PollComposeItem.Option()
|
||||||
option.text = $0.title
|
option.text = $0.title
|
||||||
return option
|
return option
|
||||||
|
|
|
@ -378,68 +378,44 @@ extension StatusView {
|
||||||
private func configurePoll(status: MastodonStatus) {
|
private func configurePoll(status: MastodonStatus) {
|
||||||
let status = status.reblog ?? status
|
let status = status.reblog ?? status
|
||||||
|
|
||||||
guard
|
guard let poll = status.poll else {
|
||||||
let context = viewModel.context?.managedObjectContext,
|
|
||||||
let domain = viewModel.authContext?.mastodonAuthenticationBox.domain,
|
|
||||||
let pollId = status.entity.poll?.id
|
|
||||||
else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let predicate = Poll.predicate(domain: domain, id: pollId)
|
let options = poll.options
|
||||||
guard let poll = Poll.findOrFetch(in: context, matching: predicate) else { return }
|
let items: [PollItem] = options.map { .option(record: $0) }
|
||||||
|
|
||||||
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)) }
|
|
||||||
self.viewModel.pollItems = items
|
self.viewModel.pollItems = items
|
||||||
|
|
||||||
// isVoteButtonEnabled
|
let hasSelectedOption = options.contains(where: { $0.isSelected == true })
|
||||||
poll.publisher(for: \.updatedAt)
|
viewModel.isVoteButtonEnabled = hasSelectedOption
|
||||||
.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
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
poll.publisher(for: \.votedBy),
|
poll.$voted,
|
||||||
poll.publisher(for: \.expired)
|
poll.$expired
|
||||||
)
|
)
|
||||||
.map { [weak viewModel] votedBy, expired in
|
.map { voted, expired in
|
||||||
guard let viewModel = viewModel else { return false }
|
return voted == false && expired == 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
|
|
||||||
}
|
}
|
||||||
.assign(to: &viewModel.$isVotable)
|
.assign(to: &viewModel.$isVotable)
|
||||||
|
|
||||||
// votesCount
|
poll.$votesCount
|
||||||
poll.publisher(for: \.votesCount)
|
|
||||||
.map { Int($0) }
|
|
||||||
.assign(to: \.voteCount, on: viewModel)
|
.assign(to: \.voteCount, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// voterCount
|
|
||||||
poll.publisher(for: \.votersCount)
|
poll.$votersCount
|
||||||
.map { Int($0) }
|
|
||||||
.assign(to: \.voterCount, on: viewModel)
|
.assign(to: \.voterCount, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// expireAt
|
|
||||||
poll.publisher(for: \.expiresAt)
|
poll.$expiresAt
|
||||||
.assign(to: \.expireAt, on: viewModel)
|
.assign(to: \.expireAt, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// expired
|
|
||||||
poll.publisher(for: \.expired)
|
poll.$expired
|
||||||
.assign(to: \.expired, on: viewModel)
|
.assign(to: \.expired, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// isVoting
|
|
||||||
poll.publisher(for: \.isVoting)
|
poll.$voted
|
||||||
|
.map { $0 == true }
|
||||||
.assign(to: \.isVoting, on: viewModel)
|
.assign(to: \.isVoting, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
|
@ -457,8 +457,32 @@ extension StatusView.ViewModel {
|
||||||
|
|
||||||
statusView.pollTableViewHeightLayoutConstraint.constant = CGFloat(items.count) * PollOptionTableViewCell.height
|
statusView.pollTableViewHeightLayoutConstraint.constant = CGFloat(items.count) * PollOptionTableViewCell.height
|
||||||
statusView.setPollDisplay()
|
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)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
$isVotable
|
$isVotable
|
||||||
.sink { isVotable in
|
.sink { isVotable in
|
||||||
statusView.pollTableView.allowsSelection = isVotable
|
statusView.pollTableView.allowsSelection = isVotable
|
||||||
|
@ -508,14 +532,17 @@ extension StatusView.ViewModel {
|
||||||
$isVotable,
|
$isVotable,
|
||||||
$isVoting
|
$isVoting
|
||||||
)
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { isVotable, isVoting in
|
.sink { isVotable, isVoting in
|
||||||
guard isVotable else {
|
guard isVotable else {
|
||||||
statusView.pollVoteButton.isHidden = true
|
statusView.pollVoteButton.isHidden = true
|
||||||
statusView.pollVoteActivityIndicatorView.isHidden = true
|
statusView.pollVoteActivityIndicatorView.isHidden = true
|
||||||
|
statusView.pollTableView.isUserInteractionEnabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
statusView.pollVoteButton.isHidden = isVoting
|
statusView.pollVoteButton.isHidden = isVoting
|
||||||
|
statusView.pollTableView.isUserInteractionEnabled = !isVoting
|
||||||
statusView.pollVoteActivityIndicatorView.isHidden = !isVoting
|
statusView.pollVoteActivityIndicatorView.isHidden = !isVoting
|
||||||
statusView.pollVoteActivityIndicatorView.startAnimating()
|
statusView.pollVoteActivityIndicatorView.startAnimating()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Meta
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
public extension CGSize {
|
public extension CGSize {
|
||||||
static let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
static let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
||||||
|
|
Loading…
Reference in New Issue