mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Vote on a poll and end a poll (#1597)
* Fix project file * Add vote and end poll actions * Handle redaction * Fix local echo behavior * Cleanup * Fix UI tests * Add localisation * Cleanup * Fix end poll action style
This commit is contained in:
@@ -56,6 +56,8 @@ public enum L10n {
|
||||
public static var actionEdit: String { return L10n.tr("Localizable", "action_edit") }
|
||||
/// Enable
|
||||
public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") }
|
||||
/// End poll
|
||||
public static var actionEndPoll: String { return L10n.tr("Localizable", "action_end_poll") }
|
||||
/// Forgot password?
|
||||
public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") }
|
||||
/// Forward
|
||||
|
||||
@@ -1485,6 +1485,48 @@ class RoomProxyMock: RoomProxyProtocol {
|
||||
return createPollQuestionAnswersPollKindReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - sendPollResponse
|
||||
|
||||
var sendPollResponsePollStartIDAnswersCallsCount = 0
|
||||
var sendPollResponsePollStartIDAnswersCalled: Bool {
|
||||
return sendPollResponsePollStartIDAnswersCallsCount > 0
|
||||
}
|
||||
var sendPollResponsePollStartIDAnswersReceivedArguments: (pollStartID: String, answers: [String])?
|
||||
var sendPollResponsePollStartIDAnswersReceivedInvocations: [(pollStartID: String, answers: [String])] = []
|
||||
var sendPollResponsePollStartIDAnswersReturnValue: Result<Void, RoomProxyError>!
|
||||
var sendPollResponsePollStartIDAnswersClosure: ((String, [String]) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, RoomProxyError> {
|
||||
sendPollResponsePollStartIDAnswersCallsCount += 1
|
||||
sendPollResponsePollStartIDAnswersReceivedArguments = (pollStartID: pollStartID, answers: answers)
|
||||
sendPollResponsePollStartIDAnswersReceivedInvocations.append((pollStartID: pollStartID, answers: answers))
|
||||
if let sendPollResponsePollStartIDAnswersClosure = sendPollResponsePollStartIDAnswersClosure {
|
||||
return await sendPollResponsePollStartIDAnswersClosure(pollStartID, answers)
|
||||
} else {
|
||||
return sendPollResponsePollStartIDAnswersReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - endPoll
|
||||
|
||||
var endPollPollStartIDCallsCount = 0
|
||||
var endPollPollStartIDCalled: Bool {
|
||||
return endPollPollStartIDCallsCount > 0
|
||||
}
|
||||
var endPollPollStartIDReceivedPollStartID: String?
|
||||
var endPollPollStartIDReceivedInvocations: [String] = []
|
||||
var endPollPollStartIDReturnValue: Result<Void, RoomProxyError>!
|
||||
var endPollPollStartIDClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func endPoll(pollStartID: String) async -> Result<Void, RoomProxyError> {
|
||||
endPollPollStartIDCallsCount += 1
|
||||
endPollPollStartIDReceivedPollStartID = pollStartID
|
||||
endPollPollStartIDReceivedInvocations.append(pollStartID)
|
||||
if let endPollPollStartIDClosure = endPollPollStartIDClosure {
|
||||
return await endPollPollStartIDClosure(pollStartID)
|
||||
} else {
|
||||
return endPollPollStartIDReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
|
||||
var updatePublisher: AnyPublisher<TimelineProviderUpdate, Never> {
|
||||
|
||||
@@ -60,7 +60,7 @@ enum RoomScreenViewAction {
|
||||
case toggleReaction(key: String, itemID: TimelineItemIdentifier)
|
||||
case sendReadReceiptIfNeeded(TimelineItemIdentifier)
|
||||
case paginateBackwards
|
||||
case selectedPollOption(poll: Poll, optionID: String)
|
||||
case selectedPollOption(pollStartID: String, optionID: String)
|
||||
|
||||
case timelineItemMenu(itemID: TimelineItemIdentifier)
|
||||
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
|
||||
|
||||
@@ -139,8 +139,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
if state.swiftUITimelineEnabled {
|
||||
renderPendingTimelineItems()
|
||||
}
|
||||
case .selectedPollOption:
|
||||
break
|
||||
case let .selectedPollOption(pollStartID, optionID):
|
||||
sendPollResponse(pollStartID: pollStartID, optionID: optionID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +489,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
actionsSubject.send(.composer(action: .removeFocus))
|
||||
state.bindings.actionMenuInfo = .init(item: eventTimelineItem)
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
@@ -510,11 +511,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
return .init(actions: [], debugActions: debugActions)
|
||||
}
|
||||
|
||||
var actions: [TimelineItemMenuAction] = [
|
||||
.reply
|
||||
]
|
||||
var actions: [TimelineItemMenuAction] = []
|
||||
|
||||
if item.isMessage {
|
||||
actions.append(.reply)
|
||||
actions.append(.forward(itemID: itemID))
|
||||
}
|
||||
|
||||
@@ -527,6 +527,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
actions.append(.copyPermalink)
|
||||
|
||||
if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = itemID.eventID {
|
||||
actions.append(.endPoll(pollStartID: eventID))
|
||||
}
|
||||
|
||||
if canRedactItem(item) {
|
||||
actions.append(.redact)
|
||||
@@ -610,6 +614,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id))
|
||||
case .react:
|
||||
showEmojiPicker(for: itemID)
|
||||
case .endPoll(let pollStartID):
|
||||
endPoll(pollStartID: pollStartID)
|
||||
}
|
||||
|
||||
if action.switchToDefaultComposer {
|
||||
@@ -791,6 +797,32 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey)
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
private func sendPollResponse(pollStartID: String, optionID: String) {
|
||||
Task {
|
||||
let sendPollResponseResult = await roomProxy.sendPollResponse(pollStartID: pollStartID, answers: [optionID])
|
||||
switch sendPollResponseResult {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
displayError(.toast(L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func endPoll(pollStartID: String) {
|
||||
Task {
|
||||
let endPollResult = await roomProxy.endPoll(pollStartID: pollStartID)
|
||||
switch endPollResult {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
displayError(.toast(L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomProxyProtocol {
|
||||
|
||||
@@ -29,14 +29,15 @@ struct PollRoomTimelineView: View {
|
||||
|
||||
ForEach(poll.options, id: \.id) { option in
|
||||
Button {
|
||||
context.send(viewAction: .selectedPollOption(poll: poll, optionID: option.id))
|
||||
guard let eventID else { return }
|
||||
context.send(viewAction: .selectedPollOption(pollStartID: eventID, optionID: option.id))
|
||||
} label: {
|
||||
PollOptionView(pollOption: option,
|
||||
showVotes: showVotes,
|
||||
isFinalResult: poll.hasEnded)
|
||||
.foregroundColor(progressBarColor(for: option))
|
||||
}
|
||||
.disabled(poll.hasEnded)
|
||||
.disabled(poll.hasEnded || eventID == nil)
|
||||
}
|
||||
|
||||
summaryView
|
||||
@@ -51,6 +52,10 @@ struct PollRoomTimelineView: View {
|
||||
timelineItem.poll
|
||||
}
|
||||
|
||||
private var eventID: String? {
|
||||
timelineItem.id.eventID
|
||||
}
|
||||
|
||||
private var questionView: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(Asset.Images.timelinePoll.name)
|
||||
@@ -102,10 +107,6 @@ private extension Poll {
|
||||
return L10n.commonPollUndisclosedText
|
||||
}
|
||||
}
|
||||
|
||||
var hasEnded: Bool {
|
||||
endDate != nil
|
||||
}
|
||||
}
|
||||
|
||||
struct PollRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemMenuActions {
|
||||
@@ -51,6 +52,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
case retryDecryption(sessionID: String)
|
||||
case report
|
||||
case react
|
||||
case endPoll(pollStartID: String)
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
@@ -107,6 +109,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
case .retryDecryption: return Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message")
|
||||
case .report: return Label(L10n.actionReportContent, systemImage: "exclamationmark.bubble")
|
||||
case .react: return Label(L10n.actionReact, systemImage: "hand.thumbsup")
|
||||
case .endPoll: return Label { Text(L10n.actionEndPoll) } icon: { Image.compound.check.resizable() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +688,8 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
@@ -699,6 +701,28 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.room.sendPollResponse(pollStartId: pollStartID, answers: answers, txnId: genTransactionId()))
|
||||
} catch {
|
||||
MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedSendingPollResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endPoll(pollStartID: String) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.room.endPoll(pollStartId: pollStartID, text: .init(), txnId: genTransactionId()))
|
||||
} catch {
|
||||
MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedEndingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
||||
|
||||
@@ -43,6 +43,8 @@ enum RoomProxyError: Error, Equatable {
|
||||
case failedUploadingAvatar
|
||||
case failedCheckingPermission
|
||||
case failedCreatingPoll
|
||||
case failedSendingPollResponse
|
||||
case failedEndingPoll
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
@@ -168,6 +170,10 @@ protocol RoomProxyProtocol {
|
||||
func canUserRedact(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func endPoll(pollStartID: String) async -> Result<Void, RoomProxyError>
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
|
||||
@@ -26,7 +26,7 @@ struct TimelineItemIdentifier: Hashable {
|
||||
/// Only available for EventTimelineItem and only when the item is returned by the server.
|
||||
var eventID: String?
|
||||
|
||||
/// Uniquely identfies the local echo of the timeline item.
|
||||
/// Uniquely identifies the local echo of the timeline item.
|
||||
/// Only available for sent EventTimelineItem that have not been returned by the server yet.
|
||||
var transactionID: String?
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ extension EventBasedTimelineItemProtocol {
|
||||
self is LocationRoomTimelineItem
|
||||
}
|
||||
|
||||
var pollIfAvailable: Poll? {
|
||||
(self as? PollRoomTimelineItem)?.poll
|
||||
}
|
||||
|
||||
var isRedacted: Bool {
|
||||
self is RedactedRoomTimelineItem
|
||||
}
|
||||
|
||||
+4
@@ -35,6 +35,10 @@ struct Poll: Equatable {
|
||||
let votes: [String: [String]]
|
||||
let endDate: Date?
|
||||
|
||||
var hasEnded: Bool {
|
||||
endDate != nil
|
||||
}
|
||||
|
||||
enum Kind: Equatable {
|
||||
case disclosed
|
||||
case undisclosed
|
||||
|
||||
Reference in New Issue
Block a user