diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8a4318286..7a5d14354 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -793,6 +793,7 @@ 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; 832A4EA1094B8FE423A08700 /* RoomChangeRolesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2A421198FD20AAAED20004 /* RoomChangeRolesScreen.swift */; }; 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; }; + 83A01FE4DACABDE97F04D338 /* PollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD507F529D440215892618A /* PollTests.swift */; }; 83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; }; 83D519C509F0F76EDBB60455 /* KnockRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */; }; 84196A8F1963D55726261879 /* RoomLiveLocationServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */; }; @@ -3070,6 +3071,7 @@ FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformCursorView.swift; sourceTree = ""; }; FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; + FBD507F529D440215892618A /* PollTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTests.swift; sourceTree = ""; }; FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceRoomDetailsCell.swift; sourceTree = ""; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = ""; }; FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = ""; }; @@ -5006,6 +5008,7 @@ 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */, 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */, 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, + FBD507F529D440215892618A /* PollTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */, 30EA681527A8FE65E4C8E9A9 /* ReportContentScreenViewModelTests.swift */, @@ -7984,6 +7987,7 @@ 3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */, 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */, FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */, + 83A01FE4DACABDE97F04D338 /* PollTests.swift in Sources */, E3EBC3BF7CE3960B41757BAA /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, 522269133E6F65F68482F4F4 /* RemotePreferenceTests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 91af059f3..3b787688a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -2,6 +2,9 @@ "screen_search_empty_state_message" = "Search for rooms"; "screen_search_empty_state_title" = "Start searching..."; "screen_search_no_results_message" = "There are no results for β€œ%1$@.” Try a new search term."; +"a11y_screen_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person"; +"a11y_screen_create_poll_votes_allowed_increase" = "Increase votes allowed per person"; +"screen_create_poll_votes_allowed_per_person" = "Votes allowed per person"; "soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages."; "soft_logout_clear_data_dialog_title" = "Clear data"; "soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account."; diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index a24c4b413..b19fc4b4b 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,12 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal nonisolated enum UntranslatedL10n { + /// Decrease votes allowed per person + internal static var a11yScreenCreatePollVotesAllowedDecrease: String { return UntranslatedL10n.tr("Untranslated", "a11y_screen_create_poll_votes_allowed_decrease") } + /// Increase votes allowed per person + internal static var a11yScreenCreatePollVotesAllowedIncrease: String { return UntranslatedL10n.tr("Untranslated", "a11y_screen_create_poll_votes_allowed_increase") } + /// Votes allowed per person + internal static var screenCreatePollVotesAllowedPerPerson: String { return UntranslatedL10n.tr("Untranslated", "screen_create_poll_votes_allowed_per_person") } /// Search internal static var screenHomeTabSearch: String { return UntranslatedL10n.tr("Untranslated", "screen_home_tab_search") } /// Search for rooms diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index cc79c7eec..fd33492da 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -9561,44 +9561,44 @@ nonisolated class PollInteractionHandlerMock: PollInteractionHandlerProtocol, @u //MARK: - sendPollResponse - private let sendPollResponsePollStartIDOptionIDCallsCountLock = NSLock() - private nonisolated(unsafe) var sendPollResponsePollStartIDOptionIDUnderlyingCallsCount = 0 - var sendPollResponsePollStartIDOptionIDCallsCount: Int { - get { sendPollResponsePollStartIDOptionIDCallsCountLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingCallsCount } } - set { sendPollResponsePollStartIDOptionIDCallsCountLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingCallsCount = newValue } } + private let sendPollResponsePollStartIDAnswerIDsCallsCountLock = NSLock() + private nonisolated(unsafe) var sendPollResponsePollStartIDAnswerIDsUnderlyingCallsCount = 0 + var sendPollResponsePollStartIDAnswerIDsCallsCount: Int { + get { sendPollResponsePollStartIDAnswerIDsCallsCountLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingCallsCount } } + set { sendPollResponsePollStartIDAnswerIDsCallsCountLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingCallsCount = newValue } } } - var sendPollResponsePollStartIDOptionIDCalled: Bool { - return sendPollResponsePollStartIDOptionIDCallsCount > 0 + var sendPollResponsePollStartIDAnswerIDsCalled: Bool { + return sendPollResponsePollStartIDAnswerIDsCallsCount > 0 } - private let sendPollResponsePollStartIDOptionIDReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var sendPollResponsePollStartIDOptionIDUnderlyingReceivedArguments: (pollStartID: String, optionID: String)? - var sendPollResponsePollStartIDOptionIDReceivedArguments: (pollStartID: String, optionID: String)? { - get { sendPollResponsePollStartIDOptionIDReceivedArgumentsLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReceivedArguments } } - set { sendPollResponsePollStartIDOptionIDReceivedArgumentsLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReceivedArguments = newValue } } + private let sendPollResponsePollStartIDAnswerIDsReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedArguments: (pollStartID: String, answerIDs: [String])? + var sendPollResponsePollStartIDAnswerIDsReceivedArguments: (pollStartID: String, answerIDs: [String])? { + get { sendPollResponsePollStartIDAnswerIDsReceivedArgumentsLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedArguments } } + set { sendPollResponsePollStartIDAnswerIDsReceivedArgumentsLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedArguments = newValue } } } - private let sendPollResponsePollStartIDOptionIDReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var sendPollResponsePollStartIDOptionIDUnderlyingReceivedInvocations: [(pollStartID: String, optionID: String)] = [] - var sendPollResponsePollStartIDOptionIDReceivedInvocations: [(pollStartID: String, optionID: String)] { - get { sendPollResponsePollStartIDOptionIDReceivedInvocationsLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReceivedInvocations } } - set { sendPollResponsePollStartIDOptionIDReceivedInvocationsLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReceivedInvocations = newValue } } + private let sendPollResponsePollStartIDAnswerIDsReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedInvocations: [(pollStartID: String, answerIDs: [String])] = [] + var sendPollResponsePollStartIDAnswerIDsReceivedInvocations: [(pollStartID: String, answerIDs: [String])] { + get { sendPollResponsePollStartIDAnswerIDsReceivedInvocationsLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedInvocations } } + set { sendPollResponsePollStartIDAnswerIDsReceivedInvocationsLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedInvocations = newValue } } } - private let sendPollResponsePollStartIDOptionIDReturnValueLock = NSLock() - private nonisolated(unsafe) var sendPollResponsePollStartIDOptionIDUnderlyingReturnValue: Result! - var sendPollResponsePollStartIDOptionIDReturnValue: Result! { - get { sendPollResponsePollStartIDOptionIDReturnValueLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReturnValue } } - set { sendPollResponsePollStartIDOptionIDReturnValueLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReturnValue = newValue } } + private let sendPollResponsePollStartIDAnswerIDsReturnValueLock = NSLock() + private nonisolated(unsafe) var sendPollResponsePollStartIDAnswerIDsUnderlyingReturnValue: Result! + var sendPollResponsePollStartIDAnswerIDsReturnValue: Result! { + get { sendPollResponsePollStartIDAnswerIDsReturnValueLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReturnValue } } + set { sendPollResponsePollStartIDAnswerIDsReturnValueLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var sendPollResponsePollStartIDOptionIDClosure: ((String, String) async -> Result)? + nonisolated(unsafe) var sendPollResponsePollStartIDAnswerIDsClosure: ((String, [String]) async -> Result)? - @concurrent func sendPollResponse(pollStartID: String, optionID: String) async -> Result { - sendPollResponsePollStartIDOptionIDCallsCountLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingCallsCount += 1 } - sendPollResponsePollStartIDOptionIDReceivedArguments = (pollStartID: pollStartID, optionID: optionID) - sendPollResponsePollStartIDOptionIDReceivedInvocationsLock.withLock { sendPollResponsePollStartIDOptionIDUnderlyingReceivedInvocations.append((pollStartID: pollStartID, optionID: optionID)) } - if let sendPollResponsePollStartIDOptionIDClosure = sendPollResponsePollStartIDOptionIDClosure { - return await sendPollResponsePollStartIDOptionIDClosure(pollStartID, optionID) + @concurrent func sendPollResponse(pollStartID: String, answerIDs: [String]) async -> Result { + sendPollResponsePollStartIDAnswerIDsCallsCountLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingCallsCount += 1 } + sendPollResponsePollStartIDAnswerIDsReceivedArguments = (pollStartID: pollStartID, answerIDs: answerIDs) + sendPollResponsePollStartIDAnswerIDsReceivedInvocationsLock.withLock { sendPollResponsePollStartIDAnswerIDsUnderlyingReceivedInvocations.append((pollStartID: pollStartID, answerIDs: answerIDs)) } + if let sendPollResponsePollStartIDAnswerIDsClosure = sendPollResponsePollStartIDAnswerIDsClosure { + return await sendPollResponsePollStartIDAnswerIDsClosure(pollStartID, answerIDs) } else { - return sendPollResponsePollStartIDOptionIDReturnValue + return sendPollResponsePollStartIDAnswerIDsReturnValue } } //MARK: - endPoll @@ -12772,86 +12772,86 @@ nonisolated class TimelineControllerMock: TimelineControllerProtocol, @unchecked } //MARK: - createPoll - private let createPollQuestionAnswersPollKindCallsCountLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingCallsCount = 0 - var createPollQuestionAnswersPollKindCallsCount: Int { - get { createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount } } - set { createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = 0 + var createPollQuestionAnswersMaxSelectionsPollKindCallsCount: Int { + get { createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount } } + set { createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = newValue } } } - var createPollQuestionAnswersPollKindCalled: Bool { - return createPollQuestionAnswersPollKindCallsCount > 0 + var createPollQuestionAnswersMaxSelectionsPollKindCalled: Bool { + return createPollQuestionAnswersMaxSelectionsPollKindCallsCount > 0 } - private let createPollQuestionAnswersPollKindReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? - var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? { - get { createPollQuestionAnswersPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedArguments } } - set { createPollQuestionAnswersPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedArguments = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments: (question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? + var createPollQuestionAnswersMaxSelectionsPollKindReceivedArguments: (question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? { + get { createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments } } + set { createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments = newValue } } } - private let createPollQuestionAnswersPollKindReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = [] - var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] { - get { createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations } } - set { createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations: [(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] = [] + var createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocations: [(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] { + get { createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations } } + set { createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations = newValue } } } - private let createPollQuestionAnswersPollKindReturnValueLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReturnValue: Result! - var createPollQuestionAnswersPollKindReturnValue: Result! { - get { createPollQuestionAnswersPollKindReturnValueLock.withLock { createPollQuestionAnswersPollKindUnderlyingReturnValue } } - set { createPollQuestionAnswersPollKindReturnValueLock.withLock { createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue: Result! + var createPollQuestionAnswersMaxSelectionsPollKindReturnValue: Result! { + get { createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue } } + set { createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result)? + nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindClosure: ((String, [String], Int, Poll.Kind) async -> Result)? - @concurrent func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount += 1 } - createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind) - createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind)) } - if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure { - return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind) + @concurrent func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount += 1 } + createPollQuestionAnswersMaxSelectionsPollKindReceivedArguments = (question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) + createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations.append((question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind)) } + if let createPollQuestionAnswersMaxSelectionsPollKindClosure = createPollQuestionAnswersMaxSelectionsPollKindClosure { + return await createPollQuestionAnswersMaxSelectionsPollKindClosure(question, answers, maxSelections, pollKind) } else { - return createPollQuestionAnswersPollKindReturnValue + return createPollQuestionAnswersMaxSelectionsPollKindReturnValue } } //MARK: - editPoll - private let editPollOriginalQuestionAnswersPollKindCallsCountLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount = 0 - var editPollOriginalQuestionAnswersPollKindCallsCount: Int { - get { editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount } } - set { editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = 0 + var editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCount: Int { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = newValue } } } - var editPollOriginalQuestionAnswersPollKindCalled: Bool { - return editPollOriginalQuestionAnswersPollKindCallsCount > 0 + var editPollOriginalQuestionAnswersMaxSelectionsPollKindCalled: Bool { + return editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCount > 0 } - private let editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? - var editPollOriginalQuestionAnswersPollKindReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? { - get { editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments } } - set { editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments: (eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArguments: (eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments = newValue } } } - private let editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] = [] - var editPollOriginalQuestionAnswersPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] { - get { editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations } } - set { editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations: [(eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] = [] + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations = newValue } } } - private let editPollOriginalQuestionAnswersPollKindReturnValueLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue: Result! - var editPollOriginalQuestionAnswersPollKindReturnValue: Result! { - get { editPollOriginalQuestionAnswersPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue } } - set { editPollOriginalQuestionAnswersPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue: Result! + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValue: Result! { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindClosure: ((String, String, [String], Poll.Kind) async -> Result)? + nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure: ((String, String, [String], Int, Poll.Kind) async -> Result)? - @concurrent func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount += 1 } - editPollOriginalQuestionAnswersPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, pollKind: pollKind) - editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations.append((eventID: eventID, question: question, answers: answers, pollKind: pollKind)) } - if let editPollOriginalQuestionAnswersPollKindClosure = editPollOriginalQuestionAnswersPollKindClosure { - return await editPollOriginalQuestionAnswersPollKindClosure(eventID, question, answers, pollKind) + @concurrent func editPoll(original eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount += 1 } + editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) + editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations.append((eventID: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind)) } + if let editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure = editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure { + return await editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure(eventID, question, answers, maxSelections, pollKind) } else { - return editPollOriginalQuestionAnswersPollKindReturnValue + return editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValue } } //MARK: - sendPollResponse @@ -13737,86 +13737,86 @@ nonisolated class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable } //MARK: - createPoll - private let createPollQuestionAnswersPollKindCallsCountLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingCallsCount = 0 - var createPollQuestionAnswersPollKindCallsCount: Int { - get { createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount } } - set { createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = 0 + var createPollQuestionAnswersMaxSelectionsPollKindCallsCount: Int { + get { createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount } } + set { createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = newValue } } } - var createPollQuestionAnswersPollKindCalled: Bool { - return createPollQuestionAnswersPollKindCallsCount > 0 + var createPollQuestionAnswersMaxSelectionsPollKindCalled: Bool { + return createPollQuestionAnswersMaxSelectionsPollKindCallsCount > 0 } - private let createPollQuestionAnswersPollKindReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? - var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? { - get { createPollQuestionAnswersPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedArguments } } - set { createPollQuestionAnswersPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedArguments = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments: (question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? + var createPollQuestionAnswersMaxSelectionsPollKindReceivedArguments: (question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? { + get { createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments } } + set { createPollQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments = newValue } } } - private let createPollQuestionAnswersPollKindReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = [] - var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] { - get { createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations } } - set { createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations: [(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] = [] + var createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocations: [(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] { + get { createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations } } + set { createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations = newValue } } } - private let createPollQuestionAnswersPollKindReturnValueLock = NSLock() - private nonisolated(unsafe) var createPollQuestionAnswersPollKindUnderlyingReturnValue: Result! - var createPollQuestionAnswersPollKindReturnValue: Result! { - get { createPollQuestionAnswersPollKindReturnValueLock.withLock { createPollQuestionAnswersPollKindUnderlyingReturnValue } } - set { createPollQuestionAnswersPollKindReturnValueLock.withLock { createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue } } + private let createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock = NSLock() + private nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue: Result! + var createPollQuestionAnswersMaxSelectionsPollKindReturnValue: Result! { + get { createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue } } + set { createPollQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result)? + nonisolated(unsafe) var createPollQuestionAnswersMaxSelectionsPollKindClosure: ((String, [String], Int, Poll.Kind) async -> Result)? - @concurrent func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - createPollQuestionAnswersPollKindCallsCountLock.withLock { createPollQuestionAnswersPollKindUnderlyingCallsCount += 1 } - createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind) - createPollQuestionAnswersPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersPollKindUnderlyingReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind)) } - if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure { - return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind) + @concurrent func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + createPollQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount += 1 } + createPollQuestionAnswersMaxSelectionsPollKindReceivedArguments = (question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) + createPollQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { createPollQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations.append((question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind)) } + if let createPollQuestionAnswersMaxSelectionsPollKindClosure = createPollQuestionAnswersMaxSelectionsPollKindClosure { + return await createPollQuestionAnswersMaxSelectionsPollKindClosure(question, answers, maxSelections, pollKind) } else { - return createPollQuestionAnswersPollKindReturnValue + return createPollQuestionAnswersMaxSelectionsPollKindReturnValue } } //MARK: - editPoll - private let editPollOriginalQuestionAnswersPollKindCallsCountLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount = 0 - var editPollOriginalQuestionAnswersPollKindCallsCount: Int { - get { editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount } } - set { editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = 0 + var editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCount: Int { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount = newValue } } } - var editPollOriginalQuestionAnswersPollKindCalled: Bool { - return editPollOriginalQuestionAnswersPollKindCallsCount > 0 + var editPollOriginalQuestionAnswersMaxSelectionsPollKindCalled: Bool { + return editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCount > 0 } - private let editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? - var editPollOriginalQuestionAnswersPollKindReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? { - get { editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments } } - set { editPollOriginalQuestionAnswersPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedArguments = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments: (eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArguments: (eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)? { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArgumentsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedArguments = newValue } } } - private let editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] = [] - var editPollOriginalQuestionAnswersPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] { - get { editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations } } - set { editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations: [(eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] = [] + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind)] { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations = newValue } } } - private let editPollOriginalQuestionAnswersPollKindReturnValueLock = NSLock() - private nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue: Result! - var editPollOriginalQuestionAnswersPollKindReturnValue: Result! { - get { editPollOriginalQuestionAnswersPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue } } - set { editPollOriginalQuestionAnswersPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReturnValue = newValue } } + private let editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock = NSLock() + private nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue: Result! + var editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValue: Result! { + get { editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue } } + set { editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValueLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var editPollOriginalQuestionAnswersPollKindClosure: ((String, String, [String], Poll.Kind) async -> Result)? + nonisolated(unsafe) var editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure: ((String, String, [String], Int, Poll.Kind) async -> Result)? - @concurrent func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - editPollOriginalQuestionAnswersPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingCallsCount += 1 } - editPollOriginalQuestionAnswersPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, pollKind: pollKind) - editPollOriginalQuestionAnswersPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersPollKindUnderlyingReceivedInvocations.append((eventID: eventID, question: question, answers: answers, pollKind: pollKind)) } - if let editPollOriginalQuestionAnswersPollKindClosure = editPollOriginalQuestionAnswersPollKindClosure { - return await editPollOriginalQuestionAnswersPollKindClosure(eventID, question, answers, pollKind) + @concurrent func editPoll(original eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + editPollOriginalQuestionAnswersMaxSelectionsPollKindCallsCountLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingCallsCount += 1 } + editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) + editPollOriginalQuestionAnswersMaxSelectionsPollKindReceivedInvocationsLock.withLock { editPollOriginalQuestionAnswersMaxSelectionsPollKindUnderlyingReceivedInvocations.append((eventID: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind)) } + if let editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure = editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure { + return await editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure(eventID, question, answers, maxSelections, pollKind) } else { - return editPollOriginalQuestionAnswersPollKindReturnValue + return editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValue } } //MARK: - sendPollResponse diff --git a/ElementX/Sources/Mocks/PollMock.swift b/ElementX/Sources/Mocks/PollMock.swift index d52ea0a3d..956ba7bfe 100644 --- a/ElementX/Sources/Mocks/PollMock.swift +++ b/ElementX/Sources/Mocks/PollMock.swift @@ -11,13 +11,14 @@ import Foundation extension Poll { static func mock(question: String, pollKind: Poll.Kind = .disclosed, + maxSelections: Int = 1, options: [Poll.Option], votes: [String: [String]] = [:], ended: Bool = false, createdByAccountOwner: Bool = false) -> Self { .init(question: question, kind: pollKind, - maxSelections: 1, + maxSelections: maxSelections, options: options, votes: votes, endDate: ended ? Date() : nil, diff --git a/ElementX/Sources/Mocks/TimelineControllerMock.swift b/ElementX/Sources/Mocks/TimelineControllerMock.swift index 10fea0ffa..a4ed0b813 100644 --- a/ElementX/Sources/Mocks/TimelineControllerMock.swift +++ b/ElementX/Sources/Mocks/TimelineControllerMock.swift @@ -176,17 +176,17 @@ struct TimelineControllerMockConfiguration { return .success(()) } - createPollQuestionAnswersPollKindClosure = { [weak self, timelineProxy] question, answers, pollKind in + createPollQuestionAnswersMaxSelectionsPollKindClosure = { [weak self, timelineProxy] question, answers, maxSelections, pollKind in self?.callbacks.send(.messageSentOrEdited) if let timelineProxy { - _ = await timelineProxy.createPoll(question: question, answers: answers, pollKind: pollKind) + _ = await timelineProxy.createPoll(question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) } return .success(()) } - editPollOriginalQuestionAnswersPollKindClosure = { [timelineProxy] eventID, question, answers, pollKind in + editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure = { [timelineProxy] eventID, question, answers, maxSelections, pollKind in if let timelineProxy { - _ = await timelineProxy.editPoll(original: eventID, question: question, answers: answers, pollKind: pollKind) + _ = await timelineProxy.editPoll(original: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind) } return .success(()) } diff --git a/ElementX/Sources/Mocks/TimelineProxyMock.swift b/ElementX/Sources/Mocks/TimelineProxyMock.swift index f8a6c859c..6c81ed3b6 100644 --- a/ElementX/Sources/Mocks/TimelineProxyMock.swift +++ b/ElementX/Sources/Mocks/TimelineProxyMock.swift @@ -29,8 +29,8 @@ extension TimelineProxyMock { paginateBackwardsRequestSizeReturnValue = .success(()) paginateForwardsRequestSizeReturnValue = .success(()) sendReadReceiptForTypeReturnValue = .success(()) - createPollQuestionAnswersPollKindReturnValue = .success(()) - editPollOriginalQuestionAnswersPollKindReturnValue = .success(()) + createPollQuestionAnswersMaxSelectionsPollKindReturnValue = .success(()) + editPollOriginalQuestionAnswersMaxSelectionsPollKindReturnValue = .success(()) if let provider = configuration.timelineItemProvider { timelineItemProvider = provider diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 69159ab48..447781c62 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -295,6 +295,9 @@ enum A11yIdentifiers { struct PollFormScreen { let addOption = "poll_form-add_option" + let maxSelections = "poll_form-max_selections" + let maxSelectionsDecrement = "poll_form-max_selections-decrement" + let maxSelectionsIncrement = "poll_form-max_selections-increment" let pollKind = "poll_form-kind" let question = "poll_form-question" let submit = "poll_form-submit" diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift index 3347c9ddf..793c5cd6e 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift @@ -77,7 +77,18 @@ enum PollFormMode: Hashable { struct PollFormScreenViewStateBindings: Equatable { var question = "" - var options: [Option] = [.init(), .init()] + var options: [Option] = [.init(), .init()] { + didSet { + maxSelections = clampedMaxSelections + } + } + + var maxSelections = 1 { + didSet { + maxSelections = clampedMaxSelections + } + } + var isUndisclosed = false struct Option: Identifiable, Equatable { @@ -86,13 +97,20 @@ struct PollFormScreenViewStateBindings: Equatable { } var hasValidContent: Bool { - !question.isEmpty && options.count >= 2 && options.allSatisfy { !$0.text.isEmpty } + !question.isEmpty && options.count >= 2 && options.allSatisfy { !$0.text.isEmpty } && maxSelections >= 1 && maxSelections <= options.count } var alertInfo: AlertInfo? static func == (lhs: PollFormScreenViewStateBindings, rhs: PollFormScreenViewStateBindings) -> Bool { - lhs.question == rhs.question && lhs.options.map(\.text) == rhs.options.map(\.text) && lhs.isUndisclosed == rhs.isUndisclosed + lhs.question == rhs.question && + lhs.options.map(\.text) == rhs.options.map(\.text) && + lhs.maxSelections == rhs.maxSelections && + lhs.isUndisclosed == rhs.isUndisclosed + } + + private var clampedMaxSelections: Int { + min(max(maxSelections, 1), max(options.count, 1)) } } @@ -100,6 +118,7 @@ extension PollFormScreenViewStateBindings { init(poll: Poll) { self.init(question: poll.question, options: poll.options.map { .init(text: $0.text) }, + maxSelections: min(max(poll.maxSelections, 1), max(poll.options.count, 1)), isUndisclosed: poll.kind == .undisclosed) } } @@ -110,4 +129,6 @@ enum PollFormScreenViewAction { case delete case deleteOption(index: Int) case addOption + case decrementMaxSelections + case incrementMaxSelections } diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift index 51e582b98..b0c1b6534 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift @@ -40,14 +40,15 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo case .submit: let question = state.bindings.question let options = state.bindings.options.map(\.text) + let maxSelections = state.bindings.maxSelections let pollKind = state.bindings.isUndisclosed ? Poll.Kind.undisclosed : .disclosed Task { switch state.mode { case .new: - await createPoll(question: question, options: options, pollKind: pollKind) + await createPoll(question: question, options: options, maxSelections: maxSelections, pollKind: pollKind) case .edit(let eventID, _): - await editPoll(pollStartID: eventID, question: question, options: options, pollKind: pollKind) + await editPoll(pollStartID: eventID, question: question, options: options, maxSelections: maxSelections, pollKind: pollKind) } } case .delete: @@ -80,13 +81,17 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo return } state.bindings.options.append(.init()) + case .decrementMaxSelections: + state.bindings.maxSelections -= 1 + case .incrementMaxSelections: + state.bindings.maxSelections += 1 } } // MARK: - Private - private func createPoll(question: String, options: [String], pollKind: Poll.Kind) async { - guard case .success = await timelineController.createPoll(question: question, answers: options, pollKind: pollKind) else { + private func createPoll(question: String, options: [String], maxSelections: Int, pollKind: Poll.Kind) async { + guard case .success = await timelineController.createPoll(question: question, answers: options, maxSelections: maxSelections, pollKind: pollKind) else { userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) return } @@ -102,8 +107,8 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo analytics.trackPollCreated(isUndisclosed: pollKind == .undisclosed, numberOfAnswers: options.count) } - private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind) async { - switch await timelineController.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) { + private func editPoll(pollStartID: String, question: String, options: [String], maxSelections: Int, pollKind: Poll.Kind) async { + switch await timelineController.editPoll(original: pollStartID, question: question, answers: options, maxSelections: maxSelections, pollKind: pollKind) { case .success: actionsSubject.send(.close) case .failure: diff --git a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift index 0ce521747..4ff865148 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift @@ -22,7 +22,7 @@ struct PollFormScreen: View { Form { questionSection optionsSection - showResultsSection + settingsSection deletePollSection } .trackAnalyticsIfNeeded(context: context) @@ -106,8 +106,16 @@ struct PollFormScreen: View { focus = nextOptionIndex.map { .option(index: $0) } } - private var showResultsSection: some View { + private var settingsSection: some View { Section { + PollFormMaxSelectionsRow(maxSelections: context.maxSelections, + maxNumberOfSelections: context.options.count) { + context.send(viewAction: .decrementMaxSelections) + } incrementAction: { + context.send(viewAction: .incrementMaxSelections) + } + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.maxSelections) + ListRow(label: .plain(title: L10n.screenCreatePollAnonymousDesc), kind: .toggle($context.isUndisclosed)) .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.pollKind) @@ -160,6 +168,55 @@ private extension View { } } +private struct PollFormMaxSelectionsRow: View { + let maxSelections: Int + let maxNumberOfSelections: Int + let decrementAction: () -> Void + let incrementAction: () -> Void + + var body: some View { + ListRow(kind: .custom { + HStack(spacing: 16) { + Text(UntranslatedL10n.screenCreatePollVotesAllowedPerPerson) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + .lineLimit(2) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack(spacing: 8) { + Button(action: decrementAction) { + Image(systemSymbol: .minus) + .frame(width: 20, height: 20) + } + .disabled(maxSelections <= 1) + .buttonStyle(.compound(.tertiary, size: .toolbarIcon)) + .accessibilityLabel(UntranslatedL10n.a11yScreenCreatePollVotesAllowedDecrease) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.maxSelectionsDecrement) + + Text("\(maxSelections)") + .font(.compound.bodyLGSemibold) + .foregroundStyle(.compound.textPrimary) + .monospacedDigit() + .frame(minWidth: 28) + .accessibilityLabel(Text(maxSelections.formatted())) + + Button(action: incrementAction) { + Image(systemSymbol: .plus) + .frame(width: 20, height: 20) + } + .disabled(maxSelections >= maxNumberOfSelections) + .buttonStyle(.compound(.tertiary, size: .toolbarIcon)) + .accessibilityLabel(UntranslatedL10n.a11yScreenCreatePollVotesAllowedIncrease) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.maxSelectionsIncrement) + } + .layoutPriority(1) + } + .padding(.horizontal, ListRowPadding.horizontal) + .padding(.vertical, ListRowPadding.vertical) + }) + } +} + private struct PollFormOptionRow: View { @Environment(\.editMode) var editMode @Binding var text: String diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift index 5d903bf79..e05a362ab 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift @@ -42,7 +42,7 @@ enum RoomPollsHistoryScreenViewAction { case filter(RoomPollsHistoryFilter) case end(pollStartID: String) case edit(pollStartID: String, poll: Poll) - case sendPollResponse(pollStartID: String, optionID: String) + case sendPollResponse(pollStartID: String, answerIDs: [String]) case loadMore } diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift index 509723f48..37c64876b 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift @@ -58,8 +58,8 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room updatePollsList(filter: filter) case .loadMore: paginateBackwards() - case .sendPollResponse(let pollStartID, let optionID): - sendPollResponse(pollStartID: pollStartID, optionID: optionID) + case .sendPollResponse(let pollStartID, let answerIDs): + sendPollResponse(pollStartID: pollStartID, answerIDs: answerIDs) } } @@ -117,10 +117,10 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room } } - private func sendPollResponse(pollStartID: String, optionID: String) { + private func sendPollResponse(pollStartID: String, answerIDs: [String]) { Task { do { - try await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID).get() + try await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, answerIDs: answerIDs).get() } catch { MXLog.error("Failed to send poll response. \(error)") displayError(message: L10n.errorUnknown) diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift index bbbc87fea..4d92ac253 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift @@ -62,9 +62,9 @@ struct RoomPollsHistoryScreen: View { PollView(poll: pollTimelineItem.item.poll, state: .full(isEditable: pollTimelineItem.item.isEditable), sender: pollTimelineItem.item.sender) { action in switch action { - case .selectOption(let optionID): + case .sendResponse(let answerIDs): guard let pollStartID = pollTimelineItem.item.id.eventID else { return } - context.send(viewAction: .sendPollResponse(pollStartID: pollStartID, optionID: optionID)) + context.send(viewAction: .sendPollResponse(pollStartID: pollStartID, answerIDs: answerIDs)) case .edit: guard let pollStartID = pollTimelineItem.item.id.eventID else { return } context.send(viewAction: .edit(pollStartID: pollStartID, poll: pollTimelineItem.item.poll)) diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 327dd711e..de0b2d57c 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -248,9 +248,9 @@ class TimelineInteractionHandler { // MARK: Polls - func sendPollResponse(pollStartID: String, optionID: String) { + func sendPollResponse(pollStartID: String, answerIDs: [String]) { Task { - let sendPollResponseResult = await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) + let sendPollResponseResult = await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, answerIDs: answerIDs) switch sendPollResponseResult { case .success: diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 7e832b34f..89382ff34 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -36,7 +36,7 @@ enum TimelineViewModelAction { } enum TimelineViewPollAction { - case selectOption(pollStartID: String, optionID: String) + case sendResponse(pollStartID: String, answerIDs: [String]) case end(pollStartID: String) case edit(pollStartID: String, poll: Poll) } diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 3739b4a05..62618d5ab 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -376,8 +376,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private func handlePollAction(_ action: TimelineViewPollAction) { switch action { - case let .selectOption(pollStartID, optionID): - timelineInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) + case let .sendResponse(pollStartID, answerIDs): + timelineInteractionHandler.sendPollResponse(pollStartID: pollStartID, answerIDs: answerIDs) case let .end(pollStartID): displayAlert(.pollEndConfirmation(pollStartID)) case .edit(let eventID, let poll): diff --git a/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift b/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift index cad35b27e..4481741cd 100644 --- a/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift @@ -10,7 +10,7 @@ import Compound import SwiftUI enum PollViewAction { - case selectOption(optionID: String) + case sendResponse(answerIDs: [String]) case edit case end } @@ -100,13 +100,13 @@ struct PollView: View { } private func isRemovePreviousSelectionHintEnabled(option: Poll.Option) -> Bool { - !poll.hasEnded && poll.hasMaxSelections && !option.isSelected + !poll.hasEnded && poll.maxSelections == 1 && poll.hasMaxSelections && !option.isSelected } private func pollOption(option: Poll.Option) -> some View { Button { - guard !option.isSelected else { return } - actionHandler(.selectOption(optionID: option.id)) + guard let answerIDs = poll.answerIDsAfterSelecting(optionID: option.id) else { return } + actionHandler(.sendResponse(answerIDs: answerIDs)) feedbackGenerator.impactOccurred() } label: { PollOptionView(pollOption: option, @@ -114,7 +114,25 @@ struct PollView: View { isFinalResult: poll.hasEnded) .foregroundColor(progressBarColor(for: option)) } - .disabled(poll.hasEnded) + .disabled(isOptionDisabled(option)) + } + + private func isOptionDisabled(_ option: Poll.Option) -> Bool { + if poll.hasEnded { + return true + } + + // Single-choice polls replace the current answer; multi-choice polls keep + // at least one answer while enforcing the max. + if poll.maxSelections == 1 { + return option.isSelected + } + + if option.isSelected { + return poll.options.filter(\.isSelected).count == 1 + } + + return poll.hasMaxSelections } @ViewBuilder diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift index a417e1641..2bae65d07 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift @@ -26,9 +26,9 @@ struct PollRoomTimelineView: View { state: state, sender: timelineItem.sender) { action in switch action { - case .selectOption(let optionID): - guard let eventID, let option = poll.options.first(where: { $0.id == optionID }), !option.isSelected else { return } - context.send(viewAction: .handlePollAction(.selectOption(pollStartID: eventID, optionID: option.id))) + case .sendResponse(let answerIDs): + guard let eventID else { return } + context.send(viewAction: .handlePollAction(.sendResponse(pollStartID: eventID, answerIDs: answerIDs))) case .edit: guard let eventID else { return } context.send(viewAction: .handlePollAction(.edit(pollStartID: eventID, poll: poll))) diff --git a/ElementX/Sources/Services/Polls/PollInteractionHandler.swift b/ElementX/Sources/Services/Polls/PollInteractionHandler.swift index e6183cf16..11e628414 100644 --- a/ElementX/Sources/Services/Polls/PollInteractionHandler.swift +++ b/ElementX/Sources/Services/Polls/PollInteractionHandler.swift @@ -17,8 +17,8 @@ class PollInteractionHandler: PollInteractionHandlerProtocol { self.timelineController = timelineController } - func sendPollResponse(pollStartID: String, optionID: String) async -> Result { - let sendPollResponseResult = await timelineController.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) + func sendPollResponse(pollStartID: String, answerIDs: [String]) async -> Result { + let sendPollResponseResult = await timelineController.sendPollResponse(pollStartID: pollStartID, answers: answerIDs) analyticsService.trackPollVote() return sendPollResponseResult.mapError { $0 } diff --git a/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift b/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift index 60871c625..6432af81f 100644 --- a/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift +++ b/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift @@ -9,7 +9,7 @@ import Foundation protocol PollInteractionHandlerProtocol { - func sendPollResponse(pollStartID: String, optionID: String) async -> Result + func sendPollResponse(pollStartID: String, answerIDs: [String]) async -> Result func endPoll(pollStartID: String) async -> Result } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index e045efbdc..7e1baceda 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -404,8 +404,8 @@ class TimelineController: TimelineControllerProtocol { // MARK: - Polls - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - switch await activeTimeline.createPoll(question: question, answers: answers, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError) { + func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + switch await activeTimeline.createPoll(question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError) { case .success: callbacks.send(.messageSentOrEdited) return .success(()) @@ -414,8 +414,8 @@ class TimelineController: TimelineControllerProtocol { } } - func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - await activeTimeline.editPoll(original: eventID, question: question, answers: answers, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError) + func editPoll(original eventID: String, question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { + await activeTimeline.editPoll(original: eventID, question: question, answers: answers, maxSelections: maxSelections, pollKind: pollKind).mapError(TimelineControllerError.timelineProxyError) } func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index 2b78f65c8..629eca07b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -137,11 +137,12 @@ protocol TimelineControllerProtocol: Sendable { // MARK: - Poll - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result + func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result func editPoll(original eventID: String, question: String, answers: [String], + maxSelections: Int, pollKind: Poll.Kind) async -> Result func sendPollResponse(pollStartID: String, answers: [String]) async -> Result diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift index 3ee0e4f0c..97a0cb297 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift @@ -43,6 +43,23 @@ nonisolated struct Poll: Hashable { options.filter(\.isSelected).count == maxSelections } + func answerIDsAfterSelecting(optionID: String) -> [String]? { + guard options.contains(where: { $0.id == optionID }) else { return nil } + + let selectedOptionIDs = options.filter(\.isSelected).map(\.id) + if selectedOptionIDs.contains(optionID) { + guard maxSelections > 1, selectedOptionIDs.count > 1 else { return nil } + return selectedOptionIDs.filter { $0 != optionID } + } + + guard maxSelections > 1 else { + return [optionID] + } + + guard selectedOptionIDs.count < maxSelections else { return nil } + return selectedOptionIDs + [optionID] + } + struct Option: Hashable { let id: String let text: String diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 4ecd46c5b..ce423928c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -494,12 +494,15 @@ final class TimelineProxy: TimelineProxyProtocol { // MARK: - Polls - func createPoll(question: String, answers: [String], + func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result { MXLog.info("Creating poll") do { - try await timeline.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind)) + try await timeline.createPoll(question: question, + answers: answers, + maxSelections: UInt8(max(1, min(maxSelections, answers.count))), + pollKind: .init(pollKind: pollKind)) MXLog.info("Finished creating poll") @@ -513,6 +516,7 @@ final class TimelineProxy: TimelineProxyProtocol { func editPoll(original eventID: String, question: String, answers: [String], + maxSelections: Int, pollKind: Poll.Kind) async -> Result { MXLog.info("Editing poll with eventID: \(eventID)") @@ -522,7 +526,7 @@ final class TimelineProxy: TimelineProxyProtocol { try await timeline.edit(eventOrTransactionId: originalEvent.eventOrTransactionId, newContent: .pollStart(pollData: .init(question: question, answers: answers, - maxSelections: 1, + maxSelections: UInt8(max(1, min(maxSelections, answers.count))), pollKind: .init(pollKind: pollKind)))) MXLog.info("Finished editing poll with eventID: \(eventID)") diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 79bb4b4e5..496d7525f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -124,11 +124,12 @@ protocol TimelineProxyProtocol: Sendable { func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async -> Result - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result + func createPoll(question: String, answers: [String], maxSelections: Int, pollKind: Poll.Kind) async -> Result func editPoll(original eventID: String, question: String, answers: [String], + maxSelections: Int, pollKind: Poll.Kind) async -> Result func sendPollResponse(pollStartID: String, answers: [String]) async -> Result diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-en-GB.png index 5aa8b1366..132f5d2c7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba717565284f34e677e6885e82eaba19a07b1009f7a104f4fa394e36fab91f34 -size 124711 +oid sha256:7b63bb88da132634b7490bcc65e70bcf7491a4a8fd96da116a4391c8019f2988 +size 131165 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-pseudo.png index 9576dba03..1d8f904e7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3973ef87c63089e471555d0a03396180fae10556566101555a3aea71fcb82137 -size 132776 +oid sha256:5b76b64f6a8ba7fbb58b265fb1024ae570d268c8b2686485a4f3009185cdbb62 +size 141152 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-en-GB.png index e4c2f7e34..929557615 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1c666e0578df65052bd04aa3cfd5794977e53e2d1bd5aa3b32264c12e5a481a -size 72536 +oid sha256:91a5da4b154e3d830aeefe321f149e5326b58c2c8e635947fcb3f43a3c65cc8c +size 77889 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-pseudo.png index 64304e975..7f9c9c2d8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.Edit-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40236a2b9ca075fecf6d15efa6ddc99039d18b292d9dcd914c7399843e186bd4 -size 83218 +oid sha256:da6a9884746b4c77fd6b38c83d377a872a9b61daf85d4cf06dde020507ecfb50 +size 96021 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-en-GB.png index d4b8c61ac..62a118ba9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab9b991b75cf2c878f7ac9276115b222ce73fa726173e24f3ddb2c12f19fa17d -size 127261 +oid sha256:02f623ab052f5e52597fc351680224550ec4c156296c0861b9aa4ae146097e3c +size 133670 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-pseudo.png index 400b689bc..0621d9e85 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56e8b0e5bfcbfe83726fc02d5c05534788144d480770a93a9db02339156b9338 -size 139917 +oid sha256:4789ada7478537bb62c9fc8342f548d3cd1b8998517f33f7c172884c7a39e731 +size 148409 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-en-GB.png index 78e0aee2c..f8f78ed90 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f060c42f3c8911ba0774fd8a243b7fd644cd986e79a170d6325b1a2f85191c0 -size 72823 +oid sha256:117c79bbaab1643281c91689d50e12f9f05b4bcb91a165330222dc8b8c4fdb91 +size 79156 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-pseudo.png index f27b4c6f1..2c3a5147c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollFormScreen.New-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5ec9841fda6f2b97516be4bf329af612dab6808a6c4f5328806764e161afcb5 -size 88352 +oid sha256:557924d719f75dad520ebbc22f25a2c0530b267b40188a0876cfdb31f881ace4 +size 99168 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-en-GB.png index 1fe629ce5..80d317f28 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d72a6670dc29f4e551f3a44b461abb7a96ac65fdb526f4d3c788ee735013237 -size 111649 +oid sha256:eec6eadd12659fc444d02f2a69b1afee006d8e174e4fddda4211b11fdc09e6d6 +size 111815 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-pseudo.png index e42c3d403..ccdd1f5bf 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4503c7655d8c9d091b9108ed64281ca25a554e2ead4c24309a063eee58c120d -size 117698 +oid sha256:b60d1e89d772ef70e667058d3a3c2cb4e02b9daa810674a27e1c60fe3eb2bfcc +size 117881 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-en-GB.png index cc755b0f7..8b6472617 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64a104c0a76e9064808a12ce5c02843374ce25cd1a99d92139d30ed33d207519 -size 67214 +oid sha256:b032f8457f7e1da3466e9066641ca74f3f4bc4739ce8bf933855bdfef4c69d38 +size 67157 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-pseudo.png index 4a4cb6900..2d40b4759 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollRoomTimelineView.Undisclosed-Bubble-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c601624b71617f5716a234685a7fb938163843da50f7db2f5072647cbd3119f -size 74024 +oid sha256:212e4d5ce6c3b7120362efc13a5bd8f801f5761e23d78cb3009812514b76e7d6 +size 74083 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-en-GB.png index cd9b16560..ffa7caca8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae3c8a487fd10ced84bbdc7344e7c747a09069923a918a50526338aeb38ef343 -size 101065 +oid sha256:d2ed9dc31a4994088e7d7f868fa7eb7d4cee524f74ab3db9ee8f13489445bb8f +size 101070 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-pseudo.png index 4215265f7..8fea53c6e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8722708cb7205ed1932599053b0197eed41b1a15461ca22a192dc89af3897a32 -size 105696 +oid sha256:445c6ee56bcfb7890881e4443476a1c8883025ebcf753d75541fe08ea00665de +size 105702 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-en-GB.png index 248b9cf6a..983dd9b61 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:711f0c1d1e191bba195a6a16dbbc9063b10f7a7938c291e21abbe2d3be64199c -size 56467 +oid sha256:ca06fd7001e9a4fc47a40e3cb343112d5faeba57df1ae69bccc9b3d77a57d7fd +size 56531 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-pseudo.png index feeaeb1d2..42739996e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/pollView.Undisclosed-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81a3dc9b25a1c38911e970c4464618d5fda782fdf7ad6cbc8a9e246deaa01b62 -size 61213 +oid sha256:b6859e48d238a1ff08f138c13d839b87120e6d9556ea9f929335e8f51286415b +size 61293 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-en-GB-0.png index ad4daf622..733146fc2 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c63b201b2931e8a222876e0c9e5a3e32b000ec83c8a903103c80be08c7c42f6 +oid sha256:4cda062114a6ae3c5b7cdb35505f17eb722d685c5613702f501c5b0f1495ff5e size 196187 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-pseudo-0.png index fad137f62..283928503 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:045214688aba9b8497231bc46d4b70f05cf73006cd7f4ce53dbf7200b3a6a930 -size 227087 +oid sha256:c54de901975dcfccdbc9d59b3bb72367cd7a78c6b5634d9b992523da58aca64b +size 227152 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-en-GB-0.png index bf98cfcdb..c76c9590e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79a3655a47e3c1ed6a6361bb2dc55bb01c9f4b803d3194481e47bdab24cbfccc -size 128547 +oid sha256:38789539575c066aef112024b9eb391ea0bfaf7380087bbac2f25028ab364186 +size 128616 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-pseudo-0.png index 9ce2075ae..817663f42 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesScreen.iPhone-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9549e8e59da3d131d54da01ae7eb04c4d9c876a3b7745b3d710cf23e3ec32c84 -size 149037 +oid sha256:29da80586d1664e8ee15dd9d8e5eaf53e107c0dd3d08f60013a079e1d21def77 +size 149093 diff --git a/UnitTests/Sources/PollFormScreenViewModelTests.swift b/UnitTests/Sources/PollFormScreenViewModelTests.swift index 6561ddb40..dadd123ef 100644 --- a/UnitTests/Sources/PollFormScreenViewModelTests.swift +++ b/UnitTests/Sources/PollFormScreenViewModelTests.swift @@ -26,6 +26,7 @@ struct PollFormScreenViewModelTests { let isEmpty = context.options.allSatisfy(\.text.isEmpty) #expect(isEmpty) #expect(context.question.isEmpty) + #expect(context.maxSelections == 1) #expect(context.viewState.isSubmitButtonDisabled) #expect(!context.viewState.bindings.isUndisclosed) @@ -44,6 +45,7 @@ struct PollFormScreenViewModelTests { #expect(context.options.count == 3) #expect(context.options.allSatisfy { !$0.text.isEmpty }) #expect(!context.question.isEmpty) + #expect(context.maxSelections == 1) #expect(context.viewState.isSubmitButtonDisabled) #expect(!context.viewState.bindings.isUndisclosed) @@ -90,22 +92,58 @@ struct PollFormScreenViewModelTests { #expect(context.alertInfo != nil) } + @Test + mutating func maxSelectionsBounds() async throws { + setupViewModel() + + context.send(viewAction: .decrementMaxSelections) + #expect(context.maxSelections == 1) + + context.send(viewAction: .incrementMaxSelections) + #expect(context.maxSelections == 2) + + context.send(viewAction: .incrementMaxSelections) + #expect(context.maxSelections == 2) + + context.send(viewAction: .addOption) + context.send(viewAction: .incrementMaxSelections) + #expect(context.maxSelections == 3) + + let deferredMaxSelections = deferFulfillment(context.observe(\.viewState.bindings.maxSelections)) { $0 == 2 } + context.send(viewAction: .deleteOption(index: 2)) + try await deferredMaxSelections.fulfill() + #expect(context.maxSelections == 2) + } + + @Test + mutating func editPollInitialMaxSelections() { + setupViewModel(mode: .edit(eventID: "foo", poll: .mock(question: "Pick two", + maxSelections: 2, + options: [.mock(text: "One"), + .mock(text: "Two"), + .mock(text: "Three")]))) + + #expect(context.maxSelections == 2) + } + @Test mutating func newPollSubmit() async throws { setupViewModel() context.question = "foo" context.options[0].text = "bla1" context.options[1].text = "bla2" + context.send(viewAction: .incrementMaxSelections) #expect(!context.viewState.isSubmitButtonDisabled) let deferred = deferFulfillment(viewModel.actions) { $0 == .close } try await confirmation { confirmation in - timelineProxy.createPollQuestionAnswersPollKindClosure = { question, options, kind in + timelineProxy.createPollQuestionAnswersMaxSelectionsPollKindClosure = { question, options, maxSelections, kind in #expect(question == "foo") #expect(options.count == 2) #expect(options[0] == "bla1") #expect(options[1] == "bla2") + #expect(maxSelections == 2) #expect(kind == .disclosed) confirmation() return .success(()) @@ -122,12 +160,13 @@ struct PollFormScreenViewModelTests { context.question = "What is your favorite country?" context.options.append(.init(text: "France πŸ‡«πŸ‡·")) + context.send(viewAction: .incrementMaxSelections) #expect(!context.viewState.isSubmitButtonDisabled) let deferred = deferFulfillment(viewModel.actions) { $0 == .close } try await confirmation { confirmation in - timelineProxy.editPollOriginalQuestionAnswersPollKindClosure = { eventID, question, options, kind in + timelineProxy.editPollOriginalQuestionAnswersMaxSelectionsPollKindClosure = { eventID, question, options, maxSelections, kind in #expect(eventID == "foo") #expect(question == "What is your favorite country?") #expect(options.count == 4) @@ -135,6 +174,7 @@ struct PollFormScreenViewModelTests { #expect(options[1] == "China πŸ‡¨πŸ‡³") #expect(options[2] == "USA πŸ‡ΊπŸ‡Έ") #expect(options[3] == "France πŸ‡«πŸ‡·") + #expect(maxSelections == 2) #expect(kind == .disclosed) confirmation() return .success(()) diff --git a/UnitTests/Sources/PollTests.swift b/UnitTests/Sources/PollTests.swift new file mode 100644 index 000000000..8c1ddeac7 --- /dev/null +++ b/UnitTests/Sources/PollTests.swift @@ -0,0 +1,74 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +@testable import ElementX +import Testing + +struct PollTests { + @Test + func singleSelectionReplacesSelectedAnswer() { + let poll = Poll.mock(question: "Question?", + maxSelections: 1, + options: [ + .mock(id: "first", text: "First", isSelected: true), + .mock(id: "second", text: "Second") + ]) + + #expect(poll.answerIDsAfterSelecting(optionID: "second") == ["second"]) + #expect(poll.answerIDsAfterSelecting(optionID: "first") == nil) + } + + @Test + func multipleSelectionAddsAnswerUntilLimit() { + let poll = Poll.mock(question: "Question?", + maxSelections: 2, + options: [ + .mock(id: "first", text: "First", isSelected: true), + .mock(id: "second", text: "Second"), + .mock(id: "third", text: "Third") + ]) + + #expect(poll.answerIDsAfterSelecting(optionID: "second") == ["first", "second"]) + } + + @Test + func multipleSelectionCanRemoveAnswerWhenMoreThanOneIsSelected() { + let poll = Poll.mock(question: "Question?", + maxSelections: 2, + options: [ + .mock(id: "first", text: "First", isSelected: true), + .mock(id: "second", text: "Second", isSelected: true), + .mock(id: "third", text: "Third") + ]) + + #expect(poll.answerIDsAfterSelecting(optionID: "first") == ["second"]) + } + + @Test + func multipleSelectionDoesNotAddAnswerOverLimit() { + let poll = Poll.mock(question: "Question?", + maxSelections: 2, + options: [ + .mock(id: "first", text: "First", isSelected: true), + .mock(id: "second", text: "Second", isSelected: true), + .mock(id: "third", text: "Third") + ]) + + #expect(poll.answerIDsAfterSelecting(optionID: "third") == nil) + } +} + +private extension Poll.Option { + static func mock(id: String, text: String, isSelected: Bool = false) -> Self { + .init(id: id, + text: text, + votes: 0, + allVotes: 0, + isSelected: isSelected, + isWinning: false) + } +} diff --git a/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift index f5224df91..aaae33953 100644 --- a/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift @@ -139,14 +139,14 @@ struct RoomPollsHistoryScreenViewModelTests { func sendPollResponse() async throws { let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true } - interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .success(()) - viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) + interactionHandler.sendPollResponsePollStartIDAnswerIDsReturnValue = .success(()) + viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", answerIDs: ["firstOptionID", "secondOptionID"])) try await deferred.fulfill() - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID") - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID") + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsCalled) + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].pollStartID == "somePollID") + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].answerIDs == ["firstOptionID", "secondOptionID"]) } @Test @@ -155,14 +155,14 @@ struct RoomPollsHistoryScreenViewModelTests { value.bindings.alertInfo != nil } - interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .failure(SDKError.generic) - viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) + interactionHandler.sendPollResponsePollStartIDAnswerIDsReturnValue = .failure(SDKError.generic) + viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", answerIDs: ["someOptionID"])) try await deferred.fulfill() - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID") - #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID") + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsCalled) + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].pollStartID == "somePollID") + #expect(interactionHandler.sendPollResponsePollStartIDAnswerIDsReceivedInvocations[0].answerIDs == ["someOptionID"]) } @Test