Interact with polls through the TimelineController as well (creation still disabled on threaded timeline as it's not yet supported by the SDK)

This commit is contained in:
Stefan Ceriu
2025-07-22 17:00:09 +03:00
committed by Stefan Ceriu
parent 324a203086
commit ffe2f726e7
8 changed files with 162 additions and 100 deletions
@@ -321,7 +321,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentMapNavigator(interactionMode: mode, timelineController: timelineController, animated: animated) presentMapNavigator(interactionMode: mode, timelineController: timelineController, animated: animated)
case (_, .presentPollForm(let mode), .pollForm): case (_, .presentPollForm(let mode), .pollForm):
presentPollForm(mode: mode) guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
fatalError()
}
presentPollForm(mode: mode, timelineController: timelineController)
case (_, .presentResolveSendFailure(let failure, let sendHandle), .resolveSendFailure): case (_, .presentResolveSendFailure(let failure, let sendHandle), .resolveSendFailure):
presentResolveSendFailure(failure: failure, sendHandle: sendHandle) presentResolveSendFailure(failure: failure, sendHandle: sendHandle)
@@ -343,7 +346,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentRoomMembersList() presentRoomMembersList()
case (.roomDetails, .presentPollsHistory, .pollsHistory): case (.roomDetails, .presentPollsHistory, .pollsHistory):
presentPollsHistory() Task { await self.presentRoomPollsHistory(animated: animated) }
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline): case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
startPinnedEventsTimelineFlow() startPinnedEventsTimelineFlow()
@@ -392,7 +395,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
replaceRoomMemberDetailsWithUserProfile(userID: userID) replaceRoomMemberDetailsWithUserProfile(userID: userID)
case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm): case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm):
presentPollForm(mode: mode) guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
fatalError()
}
presentPollForm(mode: mode, timelineController: timelineController)
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)): case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)):
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else { guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
@@ -557,7 +563,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentPollForm(let mode): case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode)) stateMachine.tryEvent(.presentPollForm(mode: mode),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationViewer(_, let geoURI, let description): case .presentLocationViewer(_, let geoURI, let description):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description)), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
@@ -642,7 +649,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentPollForm(let mode): case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode)) stateMachine.tryEvent(.presentPollForm(mode: mode),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationViewer(_, let geoURI, let description): case .presentLocationViewer(_, let geoURI, let description):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI,
description: description)), description: description)),
@@ -1034,7 +1042,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentPollForm(mode: PollFormMode) { private func presentPollForm(mode: PollFormMode, timelineController: TimelineControllerProtocol) {
let stackCoordinator = NavigationStackCoordinator() let stackCoordinator = NavigationStackCoordinator()
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode)) let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
stackCoordinator.setRootCoordinator(coordinator) stackCoordinator.setRootCoordinator(coordinator)
@@ -1055,9 +1063,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case let .submit(question, options, pollKind): case let .submit(question, options, pollKind):
switch mode { switch mode {
case .new: case .new:
createPoll(question: question, options: options, pollKind: pollKind) createPoll(question: question,
options: options,
pollKind: pollKind,
timelineController: timelineController)
case .edit(let eventID, _): case .edit(let eventID, _):
editPoll(pollStartID: eventID, question: question, options: options, pollKind: pollKind) editPoll(pollStartID: eventID,
question: question,
options: options,
pollKind: pollKind,
timelineController: timelineController)
} }
} }
} }
@@ -1068,9 +1083,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func createPoll(question: String, options: [String], pollKind: Poll.Kind) { private func createPoll(question: String, options: [String], pollKind: Poll.Kind, timelineController: TimelineControllerProtocol) {
Task { Task {
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind) let result = await timelineController.createPoll(question: question, answers: options, pollKind: pollKind)
self.analytics.trackComposer(inThread: false, self.analytics.trackComposer(inThread: false,
isEditing: false, isEditing: false,
@@ -1089,9 +1104,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind) { private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind, timelineController: TimelineControllerProtocol) {
Task { Task {
let result = await roomProxy.timeline.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) let result = await timelineController.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind)
switch result { switch result {
case .success: case .success:
@@ -1120,13 +1135,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentPollsHistory() { private func presentRoomPollsHistory(animated: Bool) async {
Task {
await asyncPresentRoomPollsHistory()
}
}
private func asyncPresentRoomPollsHistory() async {
let userID = userSession.clientProxy.userID let userID = userSession.clientProxy.userID
let timelineItemFactory = RoomTimelineItemFactory(userID: userID, let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
@@ -1138,7 +1147,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineItemFactory: timelineItemFactory, timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) mediaProvider: userSession.mediaProvider)
let parameters = RoomPollsHistoryScreenCoordinatorParameters(pollInteractionHandler: PollInteractionHandler(analyticsService: analytics, roomProxy: roomProxy), let parameters = RoomPollsHistoryScreenCoordinatorParameters(pollInteractionHandler: PollInteractionHandler(analyticsService: analytics,
timelineController: timelineController),
timelineController: timelineController) timelineController: timelineController)
let coordinator = RoomPollsHistoryScreenCoordinator(parameters: parameters) let coordinator = RoomPollsHistoryScreenCoordinator(parameters: parameters)
coordinator.actions coordinator.actions
@@ -1147,7 +1157,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .editPoll(let pollStartID, let poll): case .editPoll(let pollStartID, let poll):
stateMachine.tryEvent(.presentPollForm(mode: .edit(eventID: pollStartID, poll: poll))) stateMachine.tryEvent(.presentPollForm(mode: .edit(eventID: pollStartID, poll: poll)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -17752,76 +17752,6 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
return editPollOriginalQuestionAnswersPollKindReturnValue return editPollOriginalQuestionAnswersPollKindReturnValue
} }
} }
//MARK: - endPoll
var endPollPollStartIDTextUnderlyingCallsCount = 0
var endPollPollStartIDTextCallsCount: Int {
get {
if Thread.isMainThread {
return endPollPollStartIDTextUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = endPollPollStartIDTextUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
endPollPollStartIDTextUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
endPollPollStartIDTextUnderlyingCallsCount = newValue
}
}
}
}
var endPollPollStartIDTextCalled: Bool {
return endPollPollStartIDTextCallsCount > 0
}
var endPollPollStartIDTextReceivedArguments: (pollStartID: String, text: String)?
var endPollPollStartIDTextReceivedInvocations: [(pollStartID: String, text: String)] = []
var endPollPollStartIDTextUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var endPollPollStartIDTextReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return endPollPollStartIDTextUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = endPollPollStartIDTextUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
endPollPollStartIDTextUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
endPollPollStartIDTextUnderlyingReturnValue = newValue
}
}
}
}
var endPollPollStartIDTextClosure: ((String, String) async -> Result<Void, TimelineProxyError>)?
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError> {
endPollPollStartIDTextCallsCount += 1
endPollPollStartIDTextReceivedArguments = (pollStartID: pollStartID, text: text)
DispatchQueue.main.async {
self.endPollPollStartIDTextReceivedInvocations.append((pollStartID: pollStartID, text: text))
}
if let endPollPollStartIDTextClosure = endPollPollStartIDTextClosure {
return await endPollPollStartIDTextClosure(pollStartID, text)
} else {
return endPollPollStartIDTextReturnValue
}
}
//MARK: - sendPollResponse //MARK: - sendPollResponse
var sendPollResponsePollStartIDAnswersUnderlyingCallsCount = 0 var sendPollResponsePollStartIDAnswersUnderlyingCallsCount = 0
@@ -17892,6 +17822,76 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
return sendPollResponsePollStartIDAnswersReturnValue return sendPollResponsePollStartIDAnswersReturnValue
} }
} }
//MARK: - endPoll
var endPollPollStartIDTextUnderlyingCallsCount = 0
var endPollPollStartIDTextCallsCount: Int {
get {
if Thread.isMainThread {
return endPollPollStartIDTextUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = endPollPollStartIDTextUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
endPollPollStartIDTextUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
endPollPollStartIDTextUnderlyingCallsCount = newValue
}
}
}
}
var endPollPollStartIDTextCalled: Bool {
return endPollPollStartIDTextCallsCount > 0
}
var endPollPollStartIDTextReceivedArguments: (pollStartID: String, text: String)?
var endPollPollStartIDTextReceivedInvocations: [(pollStartID: String, text: String)] = []
var endPollPollStartIDTextUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var endPollPollStartIDTextReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return endPollPollStartIDTextUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = endPollPollStartIDTextUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
endPollPollStartIDTextUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
endPollPollStartIDTextUnderlyingReturnValue = newValue
}
}
}
}
var endPollPollStartIDTextClosure: ((String, String) async -> Result<Void, TimelineProxyError>)?
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError> {
endPollPollStartIDTextCallsCount += 1
endPollPollStartIDTextReceivedArguments = (pollStartID: pollStartID, text: text)
DispatchQueue.main.async {
self.endPollPollStartIDTextReceivedInvocations.append((pollStartID: pollStartID, text: text))
}
if let endPollPollStartIDTextClosure = endPollPollStartIDTextClosure {
return await endPollPollStartIDTextClosure(pollStartID, text)
} else {
return endPollPollStartIDTextReturnValue
}
}
//MARK: - getLoadedReplyDetails //MARK: - getLoadedReplyDetails
var getLoadedReplyDetailsEventIDUnderlyingCallsCount = 0 var getLoadedReplyDetailsEventIDUnderlyingCallsCount = 0
@@ -85,7 +85,9 @@ class TimelineInteractionHandler {
self.emojiProvider = emojiProvider self.emojiProvider = emojiProvider
self.timelineControllerFactory = timelineControllerFactory self.timelineControllerFactory = timelineControllerFactory
self.clientProxy = clientProxy self.clientProxy = clientProxy
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService,
timelineController: timelineController)
} }
// MARK: Timeline Item Action Menu // MARK: Timeline Item Action Menu
@@ -9,22 +9,22 @@ import Foundation
class PollInteractionHandler: PollInteractionHandlerProtocol { class PollInteractionHandler: PollInteractionHandlerProtocol {
let analyticsService: AnalyticsService let analyticsService: AnalyticsService
let roomProxy: JoinedRoomProxyProtocol let timelineController: TimelineControllerProtocol
init(analyticsService: AnalyticsService, roomProxy: JoinedRoomProxyProtocol) { init(analyticsService: AnalyticsService, timelineController: TimelineControllerProtocol) {
self.analyticsService = analyticsService self.analyticsService = analyticsService
self.roomProxy = roomProxy self.timelineController = timelineController
} }
func sendPollResponse(pollStartID: String, optionID: String) async -> Result<Void, Error> { func sendPollResponse(pollStartID: String, optionID: String) async -> Result<Void, Error> {
let sendPollResponseResult = await roomProxy.timeline.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) let sendPollResponseResult = await timelineController.sendPollResponse(pollStartID: pollStartID, answers: [optionID])
analyticsService.trackPollVote() analyticsService.trackPollVote()
return sendPollResponseResult.mapError { $0 } return sendPollResponseResult.mapError { $0 }
} }
func endPoll(pollStartID: String) async -> Result<Void, Error> { func endPoll(pollStartID: String) async -> Result<Void, Error> {
let endPollResult = await roomProxy.timeline.endPoll(pollStartID: pollStartID, let endPollResult = await timelineController.endPoll(pollStartID: pollStartID,
text: "The poll with event id: \(pollStartID) has ended") text: "The poll with event id: \(pollStartID) has ended")
analyticsService.trackPollEnd() analyticsService.trackPollEnd()
return endPollResult.mapError { $0 } return endPollResult.mapError { $0 }
@@ -244,6 +244,24 @@ class MockTimelineController: TimelineControllerProtocol {
return .success(()) return .success(())
} }
// MARK: - Polls
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
.success(())
}
func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
.success(())
}
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineControllerError> {
.success(())
}
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineControllerError> {
.success(())
}
// MARK: - UI Test signalling // MARK: - UI Test signalling
@@ -362,6 +362,24 @@ class TimelineController: TimelineControllerProtocol {
waveform: waveform, requestHandle: requestHandle).mapError(TimelineControllerError.timelineProxyError) waveform: waveform, requestHandle: requestHandle).mapError(TimelineControllerError.timelineProxyError)
} }
// MARK: - Polls
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
await activeTimeline.createPoll(question: question, answers: answers, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError)
}
func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
await activeTimeline.editPoll(original: eventID, question: question, answers: answers, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError)
}
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineControllerError> {
await activeTimeline.sendPollResponse(pollStartID: pollStartID, answers: answers).mapError(TimelineControllerError.timelineProxyError)
}
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineControllerError> {
await activeTimeline.endPoll(pollStartID: pollStartID, text: text).mapError(TimelineControllerError.timelineProxyError)
}
// MARK: - Private // MARK: - Private
/// The cancellable used to update the timeline items. /// The cancellable used to update the timeline items.
@@ -131,4 +131,17 @@ protocol TimelineControllerProtocol {
audioInfo: AudioInfo, audioInfo: AudioInfo,
waveform: [UInt16], waveform: [UInt16],
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineControllerError> requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineControllerError>
// MARK: - Poll
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError>
func editPoll(original eventID: String,
question: String,
answers: [String],
pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError>
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineControllerError>
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineControllerError>
} }
@@ -131,10 +131,10 @@ protocol TimelineProxyProtocol {
answers: [String], answers: [String],
pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError>
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError> func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError>
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError>
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError> func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError>
func buildMessageContentFor(_ message: String, func buildMessageContentFor(_ message: String,