From 72a84badcbd97518ba01762cfd424aa08431904b Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 18 Dec 2023 16:38:39 +0100 Subject: [PATCH] Polls history (#2244) --- ElementX.xcodeproj/project.pbxproj | 80 ++++++- .../en.lproj/Localizable.strings | 6 + .../Sources/Application/AppSettings.swift | 2 +- .../RoomFlowCoordinator.swift | 83 +++++++- ElementX/Sources/Generated/Strings.swift | 15 ++ .../Mocks/Generated/GeneratedMocks.swift | 71 +++++++ ElementX/Sources/Mocks/RoomProxyMock.swift | 1 + .../Other/AccessibilityIdentifiers.swift | 6 + .../RoomDetailsScreenCoordinator.swift | 3 + .../RoomDetailsScreenModels.swift | 2 + .../RoomDetailsScreenViewModel.swift | 4 +- .../View/RoomDetailsScreen.swift | 38 ++-- .../RoomPollsHistoryScreenCoordinator.swift | 60 ++++++ .../RoomPollsHistoryScreenModels.swift | 81 +++++++ .../RoomPollsHistoryScreenViewModel.swift | 200 ++++++++++++++++++ ...mPollsHistoryScreenViewModelProtocol.swift | 23 ++ .../View/RoomPollsHistoryScreen.swift | 174 +++++++++++++++ .../RoomScreenInteractionHandler.swift | 12 +- .../{Timeline => Polls}/PollOptionView.swift | 0 .../RoomScreen/View/Polls/PollView.swift | 174 +++++++++++++++ .../View/Timeline/PollRoomTimelineView.swift | 143 ++----------- .../View/DeveloperOptionsScreen.swift | 2 +- .../Polls/PollInteractionHandler.swift | 41 ++++ .../PollInteractionHandlerProtocol.swift | 25 +++ .../MockRoomTimelineController.swift | 13 +- .../RoomTimelineController.swift | 32 ++- .../RoomTimelineControllerProtocol.swift | 4 + .../Services/Timeline/TimelineProxy.swift | 17 ++ .../Timeline/TimelineProxyProtocol.swift | 4 + .../UITests/UITestsAppCoordinator.swift | 34 +++ .../UITests/UITestsScreenIdentifier.swift | 2 + .../RoomPollsHistoryScreenUITests.swift | 36 ++++ ...-iPad-9th-generation.roomDetailsScreen.png | 4 +- ...-generation.roomDetailsScreenDmDetails.png | 4 +- ...ration.roomDetailsScreenWithEmptyTopic.png | 4 +- ...generation.roomDetailsScreenWithInvite.png | 4 +- ...ration.roomDetailsScreenWithRoomAvatar.png | 4 +- ...neration.roomPollsHistoryEmptyLoadMore.png | 3 + ...th-generation.roomPollsHistoryLoadMore.png | 3 + .../en-GB-iPhone-14.roomDetailsScreen.png | 4 +- ...B-iPhone-14.roomDetailsScreenDmDetails.png | 4 +- ...one-14.roomDetailsScreenWithEmptyTopic.png | 4 +- ...-iPhone-14.roomDetailsScreenWithInvite.png | 4 +- ...one-14.roomDetailsScreenWithRoomAvatar.png | 4 +- ...Phone-14.roomPollsHistoryEmptyLoadMore.png | 3 + ...-GB-iPhone-14.roomPollsHistoryLoadMore.png | 3 + ...-iPad-9th-generation.roomDetailsScreen.png | 4 +- ...-generation.roomDetailsScreenDmDetails.png | 4 +- ...ration.roomDetailsScreenWithEmptyTopic.png | 4 +- ...generation.roomDetailsScreenWithInvite.png | 4 +- ...ration.roomDetailsScreenWithRoomAvatar.png | 4 +- ...neration.roomPollsHistoryEmptyLoadMore.png | 3 + ...th-generation.roomPollsHistoryLoadMore.png | 3 + .../pseudo-iPhone-14.roomDetailsScreen.png | 4 +- ...o-iPhone-14.roomDetailsScreenDmDetails.png | 4 +- ...one-14.roomDetailsScreenWithEmptyTopic.png | 4 +- ...-iPhone-14.roomDetailsScreenWithInvite.png | 4 +- ...one-14.roomDetailsScreenWithRoomAvatar.png | 4 +- ...Phone-14.roomPollsHistoryEmptyLoadMore.png | 3 + ...udo-iPhone-14.roomPollsHistoryLoadMore.png | 3 + ...RoomPollsHistoryScreenViewModelTests.swift | 189 +++++++++++++++++ .../test_pollView.Creator-disclosed.png | 3 + .../test_pollView.Creator-no-votes.png | 3 + .../PreviewTests/test_pollView.Disclosed.png | 3 + .../test_pollView.Ended-Disclosed.png | 3 + .../test_pollView.Ended-Undisclosed.png | 3 + .../test_pollView.Undisclosed.png | 3 + .../test_roomDetailsScreen.DM-Room.png | 4 +- .../test_roomDetailsScreen.Generic-Room.png | 4 +- .../test_roomDetailsScreen.Simple-Room.png | 4 +- .../test_roomPollsHistoryScreen.No-polls.png | 3 + .../test_roomPollsHistoryScreen.polls.png | 3 + changelog.d/2230.feature | 1 + 73 files changed, 1516 insertions(+), 202 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift create mode 100644 ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift rename ElementX/Sources/Screens/RoomScreen/View/{Timeline => Polls}/PollOptionView.swift (100%) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift create mode 100644 ElementX/Sources/Services/Polls/PollInteractionHandler.swift create mode 100644 ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift create mode 100644 UITests/Sources/RoomPollsHistoryScreenUITests.swift create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryEmptyLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryEmptyLoadMore.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryLoadMore.png create mode 100644 UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-disclosed.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-no-votes.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Disclosed.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Disclosed.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Undisclosed.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_pollView.Undisclosed.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.No-polls.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.polls.png create mode 100644 changelog.d/2230.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 625d32bd0..e553b976a 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ 1471A080552631358D152C18 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */; }; + 151D2477F75782C8702F2873 /* PollInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */; }; 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; 153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */; }; 155063E980E763D4910EA3CF /* Analytics+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */; }; @@ -127,6 +128,7 @@ 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; + 1EC6D1B58B24369734CD62BA /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F41A4B5C4F457AF710666 /* PollView.swift */; }; 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; 1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; }; 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; @@ -315,6 +317,7 @@ 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; }; 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; + 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */; }; 520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; @@ -410,7 +413,6 @@ 6AECC84BE14A13440120FED8 /* NSESettingsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB4F169D653296023ED65E6 /* NSESettingsProtocol.swift */; }; 6B05AA5D9BBCD6D8D63B80EB /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6F3DAD167F972702C8893 /* TimelineItemAccessibilityModifier.swift */; }; 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */; }; - 6B4BF4A6450F55939B49FAEF /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67779D9A1B797285A09B7720 /* PollOptionView.swift */; }; 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */; }; 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */; }; 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */; }; @@ -464,10 +466,13 @@ 784592335560C2E91D32D177 /* DeveloperOptionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */; }; 78A3392047E9D1C6FEA659B6 /* InvitesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */; }; 795A854F63301DC6B46217B9 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; + 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; }; + 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; }; 7A0A0929556792FB19B812C5 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84816E0D2F34E368BF64FA60 /* SessionVerificationScreen.swift */; }; 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7AEC56ADEFC5A7198A17412F /* InviteUsersScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */; }; + 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */; }; 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */; }; @@ -497,6 +502,7 @@ 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; }; 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; }; + 835B7AD20407F766C747BEC5 /* RoomPollsHistoryScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D147EB979902DBBE452EADC /* RoomPollsHistoryScreenUITests.swift */; }; 83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; @@ -554,6 +560,7 @@ 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; @@ -884,6 +891,7 @@ E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */; }; E4BAEED438A843D7B01D8069 /* CompletionSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F421E51DF00377DE1A01354 /* CompletionSuggestionView.swift */; }; E570117376826665640F0CFD /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16CAF20C9AC874A210E2DCF /* SessionVerificationScreenViewModelProtocol.swift */; }; + E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; }; E5F4C992845388B50BABACAA /* ServerSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */; }; E62EC30B39354A391E32A126 /* AudioRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */; }; E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; @@ -933,6 +941,7 @@ F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; }; F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; }; F0F82C3C848C865C3098AA52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; }; + F103924DED414ADFE398CE99 /* RoomPollsHistoryScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */; }; F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; }; F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */; }; @@ -948,6 +957,7 @@ F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD9E0FFA29EAACFF3AB9732 /* SessionVerificationScreenViewModel.swift */; }; F4971845B5C4F270F6BC5745 /* ScaledFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; + F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */; }; F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; }; F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; }; F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; @@ -1083,6 +1093,7 @@ 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenCoordinator.swift; sourceTree = ""; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = ""; }; 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; + 0D147EB979902DBBE452EADC /* RoomPollsHistoryScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenUITests.swift; sourceTree = ""; }; 0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = ""; }; 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentionalMentions.swift; sourceTree = ""; }; @@ -1180,6 +1191,7 @@ 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyMock.swift; sourceTree = ""; }; 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModel.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; + 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandlerProtocol.swift; sourceTree = ""; }; 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; @@ -1216,6 +1228,7 @@ 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = ""; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = ""; }; 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; + 317F41A4B5C4F457AF710666 /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = ""; }; 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; @@ -1280,6 +1293,7 @@ 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 42ADEA322D2089391E049535 /* InvitesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreen.swift; sourceTree = ""; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = ""; }; + 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; @@ -1330,6 +1344,7 @@ 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = ""; }; + 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelTests.swift; sourceTree = ""; }; 514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkCoordinator.swift; sourceTree = ""; }; 51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenModels.swift; sourceTree = ""; }; @@ -1405,7 +1420,6 @@ 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenUITests.swift; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; - 67779D9A1B797285A09B7720 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; 693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; @@ -1429,6 +1443,7 @@ 6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.swift; sourceTree = ""; }; 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = ""; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; + 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = ""; }; @@ -1590,6 +1605,7 @@ A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; A0A01AECFF54281CF35909A6 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; + A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = ""; }; A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; @@ -1657,6 +1673,7 @@ B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; + B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelTests.swift; sourceTree = ""; }; B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = ""; }; B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = ""; }; @@ -1778,6 +1795,7 @@ D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = ""; }; D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = ""; }; D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; + D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenCoordinator.swift; sourceTree = ""; }; D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = ""; }; D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = ""; }; @@ -1817,6 +1835,7 @@ DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; + DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = ""; }; DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenUITests.swift; sourceTree = ""; }; @@ -1885,6 +1904,7 @@ F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelTests.swift; sourceTree = ""; }; F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyle.swift; sourceTree = ""; }; + F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModel.swift; sourceTree = ""; }; F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; @@ -2115,6 +2135,7 @@ 6709362D60732DED2069AE0F /* MediaPlayer */, 6DE13A7AE6587B079F4049D7 /* Notification */, 114DC16B28140F885FD833E2 /* NotificationSettings */, + 599DFFE0805B08454E40D64A /* Polls */, 40E6246F03D1FE377BC5D963 /* Room */, 07900E9BFFD109F91B35B4C5 /* RoomMember */, BDCEF7C3BF6D09F5611CFC8B /* SecureBackup */, @@ -2762,6 +2783,15 @@ path = AppLockSetupBiometricsScreen; sourceTree = ""; }; + 45778D52AECD4EB99A289214 /* Polls */ = { + isa = PBXGroup; + children = ( + 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */, + 317F41A4B5C4F457AF710666 /* PollView.swift */, + ); + path = Polls; + sourceTree = ""; + }; 4775A7D6FBB210BF21318AD9 /* UserDetailsEditScreen */ = { isa = PBXGroup; children = ( @@ -2974,6 +3004,15 @@ path = ReportContentScreen; sourceTree = ""; }; + 599DFFE0805B08454E40D64A /* Polls */ = { + isa = PBXGroup; + children = ( + DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */, + 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */, + ); + path = Polls; + sourceTree = ""; + }; 5B2C520AB9863B8CBC8EB3CA /* SoftLogoutScreen */ = { isa = PBXGroup; children = ( @@ -3244,6 +3283,7 @@ EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */, 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */, 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */, + B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */, 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */, @@ -3387,6 +3427,7 @@ A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */, 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */, 874A1842477895F199567BD7 /* TimelineView.swift */, + 45778D52AECD4EB99A289214 /* Polls */, 4820FFB9F4FDDFD95763D498 /* ReadReceipts */, 1D8572B713A11CFDBF009B2F /* Replies */, A312471EA62EFB0FD94E60DC /* Style */, @@ -3713,6 +3754,7 @@ 0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */, + 0D147EB979902DBBE452EADC /* RoomPollsHistoryScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */, 1CC09F30B0E1010951952BDC /* SecureBackupLogoutConfirmationScreenUITests.swift */, @@ -4089,7 +4131,6 @@ 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */, B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */, - 67779D9A1B797285A09B7720 /* PollOptionView.swift */, C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */, B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */, C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */, @@ -4306,6 +4347,14 @@ path = Layout; sourceTree = ""; }; + D4B487C81A239A9C71807601 /* View */ = { + isa = PBXGroup; + children = ( + 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */, + ); + path = View; + sourceTree = ""; + }; D4DB8163C10389C069458252 /* RoomMemberListScreen */ = { isa = PBXGroup; children = ( @@ -4326,6 +4375,18 @@ path = View; sourceTree = ""; }; + D57B3BC211BB74420C9138D7 /* RoomPollsHistoryScreen */ = { + isa = PBXGroup; + children = ( + D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */, + 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */, + F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */, + A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */, + D4B487C81A239A9C71807601 /* View */, + ); + path = RoomPollsHistoryScreen; + sourceTree = ""; + }; D977D4E565C06D3F41C8F8FC /* Virtual */ = { isa = PBXGroup; children = ( @@ -4425,6 +4486,7 @@ B86CF59E083C82C2A842E4AD /* RoomMemberDetailsScreen */, D4DB8163C10389C069458252 /* RoomMemberListScreen */, 0210F4932B59277E2EEEF7BC /* RoomNotificationSettingsScreen */, + D57B3BC211BB74420C9138D7 /* RoomPollsHistoryScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, 2565414373E6F68005966B8E /* SecureBackup */, 3153FCA3F4B0E88B16D99D12 /* SessionVerificationScreen */, @@ -5189,6 +5251,7 @@ 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */, CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */, E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */, + 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */, @@ -5602,10 +5665,13 @@ 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */, F9EA79092C18A8CFE4922DD2 /* PollFormScreenViewModel.swift in Sources */, 260FFC1475EE94F641C3F3F9 /* PollFormScreenViewModelProtocol.swift in Sources */, + 151D2477F75782C8702F2873 /* PollInteractionHandler.swift in Sources */, + 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */, 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */, - 6B4BF4A6450F55939B49FAEF /* PollOptionView.swift in Sources */, + F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */, 864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */, 153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */, + 1EC6D1B58B24369734CD62BA /* PollView.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */, 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */, @@ -5668,6 +5734,11 @@ E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */, BA4C9049BC96DED3A2F3B82E /* RoomNotificationSettingsScreenViewModelProtocol.swift in Sources */, 491D62ACD19E6F134B1766AF /* RoomNotificationSettingsUserDefinedScreen.swift in Sources */, + 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */, + E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */, + 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */, + 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */, + F103924DED414ADFE398CE99 /* RoomPollsHistoryScreenViewModelProtocol.swift in Sources */, 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */, BD203FC6A7AE7637EA003643 /* RoomProxyMock.swift in Sources */, FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */, @@ -5937,6 +6008,7 @@ A8771F5975A82759FA5138AE /* RoomMemberDetailsScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, 06AA515C7053FD7E17A5CF81 /* RoomNotificationSettingsScreenUITests.swift in Sources */, + 835B7AD20407F766C747BEC5 /* RoomPollsHistoryScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */, F421FD5979EF53C8204BDC77 /* SecureBackupLogoutConfirmationScreenUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 6de31eb48..1673d03c3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -94,6 +94,7 @@ "action_try_again" = "Try again"; "action_view_source" = "View source"; "action_yes" = "Yes"; +"action.load_more" = "Load more"; "common_about" = "About"; "common_acceptable_use_policy" = "Acceptable use policy"; "common_advanced_settings" = "Advanced settings"; @@ -444,6 +445,11 @@ "screen_onboarding_welcome_message" = "Welcome to the fastest Element ever. Supercharged for speed and simplicity."; "screen_onboarding_welcome_subtitle" = "Welcome to %1$@. Supercharged, for speed and simplicity."; "screen_onboarding_welcome_title" = "Be in your element"; +"screen_polls_history_empty_ongoing" = "Can't find any ongoing polls."; +"screen_polls_history_empty_past" = "Can't find any past polls."; +"screen_polls_history_filter_ongoing" = "Ongoing"; +"screen_polls_history_filter_past" = "Past"; +"screen_polls_history_title" = "Polls"; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_generate_key" = "Generate a new recovery key"; "screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 0d90b729d..9096173f5 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -264,7 +264,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.chatBackupEnabled, defaultValue: false, storageType: .userDefaults(store)) var chatBackupEnabled - + #endif // MARK: - Shared diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 4ebf14fd2..97b8a1453 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -206,12 +206,22 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .mapNavigator(roomID: roomID) case (.mapNavigator(let roomID), .dismissMapNavigator): return .room(roomID: roomID) - + case (.room(let roomID), .presentPollForm): return .pollForm(roomID: roomID) case (.pollForm(let roomID), .dismissPollForm): return .room(roomID: roomID) - + + case (.roomDetails(let roomID, _), .presentPollsHistory): + return .pollsHistory(roomID: roomID) + case (.pollsHistory(let roomID), .dismissPollsHistory): + return .roomDetails(roomID: roomID, isRoot: false) + + case (.pollsHistory(let roomID), .presentPollForm): + return .pollsHistoryForm(roomID: roomID) + case (.pollsHistoryForm(let roomID), .dismissPollForm): + return .pollsHistory(roomID: roomID) + default: return nil } @@ -320,6 +330,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.pollForm, .dismissPollForm, .room): break + case (.roomDetails(let roomID, _), .presentPollsHistory, .pollsHistory): + presentPollsHistory(roomID: roomID) + case (.pollsHistory, .dismissPollsHistory, .roomDetails): + break + + case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm): + presentPollForm(mode: mode) + case (.pollsHistoryForm, .dismissPollForm, .pollsHistory): + break + default: fatalError("Unknown transition: \(context)") } @@ -516,6 +536,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentNotificationSettingsScreen) case .presentInviteUsersScreen: stateMachine.tryEvent(.presentInviteUsersScreen) + case .presentPollsHistory: + stateMachine.tryEvent(.presentPollsHistory) } } .store(in: &cancellables) @@ -845,6 +867,58 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } + private func presentPollsHistory(roomID: String) { + Task { + await asyncPresentRoomPollsHistory(roomID: roomID) + } + } + + private func asyncPresentRoomPollsHistory(roomID: String) async { + let roomProxy: RoomProxyProtocol + + guard let proxy = await userSession.clientProxy.roomForIdentifier(roomID) else { + MXLog.error("Invalid room identifier: \(roomID)") + stateMachine.tryEvent(.dismissPollsHistory) + return + } + + roomProxy = proxy + + await roomProxy.subscribeForUpdates() + + let userID = userSession.clientProxy.userID + + let timelineItemFactory = RoomTimelineItemFactory(userID: userID, + mediaProvider: userSession.mediaProvider, + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL, + mentionBuilder: MentionBuilder()), + stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID), + appSettings: appSettings) + + let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, + timelineItemFactory: timelineItemFactory, + secureBackupController: userSession.clientProxy.secureBackupController) + + let parameters = RoomPollsHistoryScreenCoordinatorParameters(roomProxy: roomProxy, + pollInteractionHandler: PollInteractionHandler(analyticsService: analytics, roomProxy: roomProxy), + roomTimelineController: roomTimelineController) + let coordinator = RoomPollsHistoryScreenCoordinator(parameters: parameters) + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .editPoll(let pollStartID, let poll): + stateMachine.tryEvent(.presentPollForm(mode: .edit(eventID: pollStartID, poll: poll))) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissPollsHistory) + } + } + private func presentRoomMemberDetails(member: RoomMemberProxyProtocol) { guard let roomProxy else { fatalError() @@ -1106,6 +1180,8 @@ private extension RoomFlowCoordinator { case messageForwarding(roomID: String, itemID: TimelineItemIdentifier) case reportContent(roomID: String, itemID: TimelineItemIdentifier, senderID: String) case pollForm(roomID: String) + case pollsHistory(roomID: String) + case pollsHistoryForm(roomID: String) } struct EventUserInfo { @@ -1158,6 +1234,9 @@ private extension RoomFlowCoordinator { case presentPollForm(mode: PollFormMode) case dismissPollForm + + case presentPollsHistory + case dismissPollsHistory } } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 65d4cfddd..cb69a00b2 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1084,6 +1084,16 @@ public enum L10n { } /// Be in your element public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") } + /// Can't find any ongoing polls. + public static var screenPollsHistoryEmptyOngoing: String { return L10n.tr("Localizable", "screen_polls_history_empty_ongoing") } + /// Can't find any past polls. + public static var screenPollsHistoryEmptyPast: String { return L10n.tr("Localizable", "screen_polls_history_empty_past") } + /// Ongoing + public static var screenPollsHistoryFilterOngoing: String { return L10n.tr("Localizable", "screen_polls_history_filter_ongoing") } + /// Past + public static var screenPollsHistoryFilterPast: String { return L10n.tr("Localizable", "screen_polls_history_filter_past") } + /// Polls + public static var screenPollsHistoryTitle: String { return L10n.tr("Localizable", "screen_polls_history_title") } /// Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work. public static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } /// Generate a new recovery key @@ -1626,6 +1636,11 @@ public enum L10n { public static var testLanguageIdentifier: String { return L10n.tr("Localizable", "test_language_identifier") } /// en public static var testUntranslatedDefaultLanguageIdentifier: String { return L10n.tr("Localizable", "test_untranslated_default_language_identifier") } + + public enum Action { + /// Load more + public static var loadMore: String { return L10n.tr("Localizable", "action.load_more") } + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 5f47be148..a8de00dcf 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1632,6 +1632,51 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { } } } +class PollInteractionHandlerMock: PollInteractionHandlerProtocol { + + //MARK: - sendPollResponse + + var sendPollResponsePollStartIDOptionIDCallsCount = 0 + var sendPollResponsePollStartIDOptionIDCalled: Bool { + return sendPollResponsePollStartIDOptionIDCallsCount > 0 + } + var sendPollResponsePollStartIDOptionIDReceivedArguments: (pollStartID: String, optionID: String)? + var sendPollResponsePollStartIDOptionIDReceivedInvocations: [(pollStartID: String, optionID: String)] = [] + var sendPollResponsePollStartIDOptionIDReturnValue: Result! + var sendPollResponsePollStartIDOptionIDClosure: ((String, String) async -> Result)? + + func sendPollResponse(pollStartID: String, optionID: String) async -> Result { + sendPollResponsePollStartIDOptionIDCallsCount += 1 + sendPollResponsePollStartIDOptionIDReceivedArguments = (pollStartID: pollStartID, optionID: optionID) + sendPollResponsePollStartIDOptionIDReceivedInvocations.append((pollStartID: pollStartID, optionID: optionID)) + if let sendPollResponsePollStartIDOptionIDClosure = sendPollResponsePollStartIDOptionIDClosure { + return await sendPollResponsePollStartIDOptionIDClosure(pollStartID, optionID) + } else { + return sendPollResponsePollStartIDOptionIDReturnValue + } + } + //MARK: - endPoll + + var endPollPollStartIDCallsCount = 0 + var endPollPollStartIDCalled: Bool { + return endPollPollStartIDCallsCount > 0 + } + var endPollPollStartIDReceivedPollStartID: String? + var endPollPollStartIDReceivedInvocations: [String] = [] + var endPollPollStartIDReturnValue: Result! + var endPollPollStartIDClosure: ((String) async -> Result)? + + func endPoll(pollStartID: String) async -> Result { + endPollPollStartIDCallsCount += 1 + endPollPollStartIDReceivedPollStartID = pollStartID + endPollPollStartIDReceivedInvocations.append(pollStartID) + if let endPollPollStartIDClosure = endPollPollStartIDClosure { + return await endPollPollStartIDClosure(pollStartID) + } else { + return endPollPollStartIDReturnValue + } + } +} class RoomMemberProxyMock: RoomMemberProxyProtocol { var userID: String { get { return underlyingUserID } @@ -2402,6 +2447,11 @@ class TimelineProxyMock: TimelineProxyProtocol { set(value) { underlyingTimelineProvider = value } } var underlyingTimelineProvider: RoomTimelineProviderProtocol! + var timelineStartReached: Bool { + get { return underlyingTimelineStartReached } + set(value) { underlyingTimelineStartReached = value } + } + var underlyingTimelineStartReached: Bool! //MARK: - subscribeForUpdates @@ -2523,6 +2573,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } //MARK: - paginateBackwards + var paginateBackwardsRequestSizeCallsCount = 0 + var paginateBackwardsRequestSizeCalled: Bool { + return paginateBackwardsRequestSizeCallsCount > 0 + } + var paginateBackwardsRequestSizeReceivedRequestSize: UInt? + var paginateBackwardsRequestSizeReceivedInvocations: [UInt] = [] + var paginateBackwardsRequestSizeReturnValue: Result! + var paginateBackwardsRequestSizeClosure: ((UInt) async -> Result)? + + func paginateBackwards(requestSize: UInt) async -> Result { + paginateBackwardsRequestSizeCallsCount += 1 + paginateBackwardsRequestSizeReceivedRequestSize = requestSize + paginateBackwardsRequestSizeReceivedInvocations.append(requestSize) + if let paginateBackwardsRequestSizeClosure = paginateBackwardsRequestSizeClosure { + return await paginateBackwardsRequestSizeClosure(requestSize) + } else { + return paginateBackwardsRequestSizeReturnValue + } + } + //MARK: - paginateBackwards + var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0 var paginateBackwardsRequestSizeUntilNumberOfItemsCalled: Bool { return paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount > 0 diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index b1e205f4a..01ffbecbe 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -35,6 +35,7 @@ struct RoomProxyMockConfiguration { var timeline = { let mock = TimelineProxyMock() mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher() + mock.timelineStartReached = false return mock }() diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 8accb7818..64e1cf50a 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -46,6 +46,7 @@ enum A11yIdentifiers { static let notificationSettingsScreen = NotificationSettingsScreen() static let notificationSettingsEditScreen = NotificationSettingsEditScreen() static let pollFormScreen = PollFormScreen() + static let roomPollsHistoryScreen = RoomPollsHistoryScreen() struct AlertInfo { let primaryButton = "alert_info-primary_button" @@ -169,6 +170,7 @@ enum A11yIdentifiers { let people = "room_details-people" let invite = "room_details-invite" let notifications = "room_details-notifications" + let pollsHistory = "romm_details-polls_history" } struct RoomMemberDetailsScreen { @@ -263,4 +265,8 @@ enum A11yIdentifiers { "\(roomNamePrefix):\(name)" } } + + struct RoomPollsHistoryScreen { + let loadMore = "room_polls_history_screen-load_more" + } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 5c30680d7..2a9a800e1 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -32,6 +32,7 @@ enum RoomDetailsScreenCoordinatorAction { case presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) case presentNotificationSettingsScreen case presentInviteUsersScreen + case presentPollsHistory } final class RoomDetailsScreenCoordinator: CoordinatorProtocol { @@ -71,6 +72,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentRoomDetailsEditScreen(accountOwner: accountOwner)) case .requestNotificationSettingsPresentation: actionsSubject.send(.presentNotificationSettingsScreen) + case .requestPollsHistoryPresentation: + actionsSubject.send(.presentPollsHistory) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 1b18ba880..030a2ecde 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -27,6 +27,7 @@ enum RoomDetailsScreenViewModelAction { case requestInvitePeoplePresentation case leftRoom case requestEditDetailsPresentation(RoomMemberProxyProtocol) + case requestPollsHistoryPresentation } // MARK: View @@ -174,6 +175,7 @@ enum RoomDetailsScreenViewAction { case processTapNotifications case processToogleMuteNotifications case displayAvatar + case processTapPolls } enum RoomDetailsScreenViewShortcut { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 7bb581819..8d4847aa0 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -67,7 +67,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr notificationSettingsState: .loading, bindings: .init()), imageProvider: mediaProvider) - + setupRoomSubscription() fetchMembers() @@ -120,6 +120,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr Task { await toggleMuteNotifications() } case .displayAvatar: displayFullScreenAvatar() + case .processTapPolls: + actionsSubject.send(.requestPollsHistoryPresentation) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 471d20de5..f73f50bf0 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -34,9 +34,7 @@ struct RoomDetailsScreen: View { notificationSection - if context.viewState.dmRecipient == nil { - aboutSection - } + aboutSection securitySection @@ -150,22 +148,30 @@ struct RoomDetailsScreen: View { private var aboutSection: some View { Section { - ListRow(label: .default(title: L10n.commonPeople, - icon: CompoundIcon(asset: Asset.Images.user)), - details: .title(String(context.viewState.joinedMembersCount)), - kind: .navigationLink { - context.send(viewAction: .processTapPeople) - }) - .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) - - if context.viewState.canInviteUsers { - ListRow(label: .default(title: L10n.screenRoomDetailsInvitePeopleTitle, - icon: CompoundIcon(asset: Asset.Images.userAdd)), + if context.viewState.dmRecipient == nil { + ListRow(label: .default(title: L10n.commonPeople, + icon: CompoundIcon(asset: Asset.Images.user)), + details: .title(String(context.viewState.joinedMembersCount)), kind: .navigationLink { - context.send(viewAction: .processTapInvite) + context.send(viewAction: .processTapPeople) }) - .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite) + .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) + + if context.viewState.canInviteUsers { + ListRow(label: .default(title: L10n.screenRoomDetailsInvitePeopleTitle, + icon: CompoundIcon(asset: Asset.Images.userAdd)), + kind: .navigationLink { + context.send(viewAction: .processTapInvite) + }) + .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite) + } } + ListRow(label: .default(title: L10n.screenPollsHistoryTitle, + icon: CompoundIcon(asset: Asset.Images.polls)), + kind: .navigationLink { + context.send(viewAction: .processTapPolls) + }) + .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.pollsHistory) } } diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift new file mode 100644 index 000000000..ea69c6156 --- /dev/null +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift @@ -0,0 +1,60 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct RoomPollsHistoryScreenCoordinatorParameters { + let roomProxy: RoomProxyProtocol + let pollInteractionHandler: PollInteractionHandlerProtocol + let roomTimelineController: RoomTimelineControllerProtocol +} + +enum RoomPollsHistoryScreenCoordinatorAction { + case editPoll(pollStartID: String, poll: Poll) +} + +final class RoomPollsHistoryScreenCoordinator: CoordinatorProtocol { + private var viewModel: RoomPollsHistoryScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: RoomPollsHistoryScreenCoordinatorParameters) { + viewModel = RoomPollsHistoryScreenViewModel(roomProxy: parameters.roomProxy, + pollInteractionHandler: parameters.pollInteractionHandler, + roomTimelineController: parameters.roomTimelineController, + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } + + func start() { + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .editPoll(let pollStartID, let poll): + actionsSubject.send(.editPoll(pollStartID: pollStartID, poll: poll)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(RoomPollsHistoryScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift new file mode 100644 index 000000000..4b12e5b96 --- /dev/null +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift @@ -0,0 +1,81 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum RoomPollsHistoryScreenViewModelAction { + case editPoll(pollStartID: String, poll: Poll) +} + +struct RoomPollsHistoryScreenViewState: BindableState { + let title: String + let filters: [RoomPollsHistoryFilter] = [.ongoing, .past] + var pollTimelineItems: [RoomPollsHistoryPollDetails] = [] + var canBackPaginate = false + var isBackPaginating = false + var bindings: RoomPollsHistoryScreenViewStateBindings + + var emptyStateMessage: String { + switch bindings.filter { + case .ongoing: + L10n.screenPollsHistoryEmptyOngoing + case .past: + L10n.screenPollsHistoryEmptyPast + } + } +} + +struct RoomPollsHistoryScreenViewStateBindings { + /// Polls list filter + var filter: RoomPollsHistoryFilter + + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum RoomPollsHistoryScreenViewAction { + case filter(RoomPollsHistoryFilter) + case end(pollStartID: String) + case edit(pollStartID: String, poll: Poll) + case sendPollResponse(pollStartID: String, optionID: String) + case loadMore +} + +enum RoomPollsHistoryFilter: Equatable { + case ongoing + case past +} + +struct RoomPollsHistoryPollDetails { + let timestamp: Date + let item: PollRoomTimelineItem +} + +extension RoomPollsHistoryFilter: CustomStringConvertible { + var description: String { + switch self { + case .ongoing: + L10n.screenPollsHistoryFilterOngoing + case .past: + L10n.screenPollsHistoryFilterPast + } + } +} + +enum RoomPollsHistoryScreenErrorType: Hashable { + /// A specific error message shown in an alert. + case alert +} diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift new file mode 100644 index 000000000..8485e1b4d --- /dev/null +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift @@ -0,0 +1,200 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Algorithms +import Combine +import OrderedCollections +import SwiftUI + +typealias RoomPollsHistoryScreenViewModelType = StateStoreViewModel + +class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, RoomPollsHistoryScreenViewModelProtocol { + private enum Constants { + static let backPaginationEventLimit: UInt = 250 + } + + private let roomProxy: RoomProxyProtocol + private let pollInteractionHandler: PollInteractionHandlerProtocol + private let roomTimelineController: RoomTimelineControllerProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private var paginateBackwardsTask: Task? + private let isPaginatingIndicatorID = UUID().uuidString + + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(roomProxy: RoomProxyProtocol, + pollInteractionHandler: PollInteractionHandlerProtocol, + roomTimelineController: RoomTimelineControllerProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + self.roomProxy = roomProxy + self.pollInteractionHandler = pollInteractionHandler + self.roomTimelineController = roomTimelineController + self.userIndicatorController = userIndicatorController + + super.init(initialViewState: RoomPollsHistoryScreenViewState(title: L10n.screenPollsHistoryTitle, + canBackPaginate: true, + bindings: .init(filter: .ongoing))) + + setupSubscriptions() + updatePollsList(filter: state.bindings.filter) + } + + // MARK: - Public + + override func process(viewAction: RoomPollsHistoryScreenViewAction) { + switch viewAction { + case .edit(let pollStartID, let poll): + actionsSubject.send(.editPoll(pollStartID: pollStartID, poll: poll)) + case .end(let pollStartID): + endPoll(pollStartID: pollStartID) + case .filter(let filter): + updatePollsList(filter: filter) + case .loadMore: + paginateBackwards() + case .sendPollResponse(let pollStartID, let optionID): + sendPollResponse(pollStartID: pollStartID, optionID: optionID) + } + } + + // MARK: - Private + + private func setupSubscriptions() { + roomTimelineController.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self else { return } + + switch callback { + case .updatedTimelineItems: + self.updatePollsList(filter: state.bindings.filter) + case .canBackPaginate(let canBackPaginate): + if self.state.canBackPaginate != canBackPaginate { + self.state.canBackPaginate = canBackPaginate + } + case .isBackPaginating: + break + } + } + .store(in: &cancellables) + + context.$viewState + .map(\.isBackPaginating) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] isBackPaginating in + guard let self else { return } + if isBackPaginating { + userIndicatorController.submitIndicator(.init(id: isPaginatingIndicatorID, type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), title: L10n.commonLoading)) + } else { + userIndicatorController.retractIndicatorWithId(isPaginatingIndicatorID) + } + } + .store(in: &cancellables) + } + + private func displayError(message: String) { + state.bindings.alertInfo = .init(id: .alert, title: message) + } + + // MARK: - Poll Interaction Handler + + private func endPoll(pollStartID: String) { + Task { + do { + try await pollInteractionHandler.endPoll(pollStartID: pollStartID).get() + } catch { + MXLog.error("Failed to end poll. \(error)") + displayError(message: L10n.errorUnknown) + } + } + } + + private func sendPollResponse(pollStartID: String, optionID: String) { + Task { + do { + try await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID).get() + } catch { + MXLog.error("Failed to send poll response. \(error)") + displayError(message: L10n.errorUnknown) + } + } + } + + // MARK: - Timeline + + private func updatePollsList(filter: RoomPollsHistoryFilter) { + // Get the poll timeline items to display + var items: [PollRoomTimelineItem] = [] + for timelineItem in roomTimelineController.timelineItems { + if let pollRoomTimelineItem = timelineItem as? PollRoomTimelineItem { + // Apply the filter + switch filter { + case .ongoing where !pollRoomTimelineItem.poll.hasEnded: + items.append(pollRoomTimelineItem) + case .past where pollRoomTimelineItem.poll.hasEnded: + items.append(pollRoomTimelineItem) + default: + break + } + } + } + + // Map into RoomPollsHistoryPollDetails to have both the event timestamp and the timeline item + state.pollTimelineItems = items.map { item in + guard let timestamp = roomTimelineController.eventTimestamp(for: item.id) else { + return nil + } + return RoomPollsHistoryPollDetails(timestamp: timestamp, item: item) + } + .compactMap { $0 } + .sorted { $0.timestamp > $1.timestamp } + } + + private func paginateBackwards() { + guard paginateBackwardsTask == nil else { + return + } + + paginateBackwardsTask = Task { [weak self] in + guard let self else { + return + } + state.isBackPaginating = true + switch await roomTimelineController.paginateBackwards(requestSize: Constants.backPaginationEventLimit) { + case .failure(let error): + MXLog.error("failed to back paginate. \(error)") + default: + break + } + paginateBackwardsTask = nil + state.isBackPaginating = false + } + } +} + +// MARK: - Mocks + +extension RoomPollsHistoryScreenViewModel { + static let mock = RoomPollsHistoryScreenViewModel(roomProxy: RoomProxyMock(), + pollInteractionHandler: PollInteractionHandlerMock(), + roomTimelineController: MockRoomTimelineController(), + userIndicatorController: UserIndicatorControllerMock()) +} diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModelProtocol.swift new file mode 100644 index 000000000..0e76e2f19 --- /dev/null +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol RoomPollsHistoryScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: RoomPollsHistoryScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift new file mode 100644 index 000000000..2212cffb9 --- /dev/null +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift @@ -0,0 +1,174 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import SwiftUI + +struct RoomPollsHistoryScreen: View { + @ObservedObject var context: RoomPollsHistoryScreenViewModel.Context + + var body: some View { + ScrollView { + VStack(alignment: .center, spacing: 16) { + modePicker + + polls + + if context.viewState.pollTimelineItems.isEmpty { + emptyStateMessage + .padding(.top, 48) + } + + if context.viewState.canBackPaginate { + loadMoreButton + .padding(.top, context.viewState.pollTimelineItems.isEmpty ? 0 : 16) + } + } + .padding() + } + .alert(item: $context.alertInfo) + .scrollContentBackground(.hidden) + .background(.compound.bgSubtleSecondaryLevel0) + .navigationTitle(context.viewState.title) + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: - Private + + private var modePicker: some View { + Picker("", selection: $context.filter) { + ForEach(context.viewState.filters, id: \.self) { filter in + Text(filter.description) + } + } + .pickerStyle(.segmented) + .readableFrame(maxWidth: 475) + .onChange(of: context.filter) { value in + context.send(viewAction: .filter(value)) + } + } + + private var polls: some View { + ForEach(context.viewState.pollTimelineItems, id: \.item.id.eventID) { pollTimelineItem in + VStack(alignment: .leading, spacing: 8) { + Text(DateFormatter.pollTimestamp.string(from: pollTimelineItem.timestamp)) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + PollView(poll: pollTimelineItem.item.poll, editable: pollTimelineItem.item.isEditable) { action in + switch action { + case .selectOption(let optionID): + guard let pollStartID = pollTimelineItem.item.id.eventID else { return } + context.send(viewAction: .sendPollResponse(pollStartID: pollStartID, optionID: optionID)) + case .edit: + guard let pollStartID = pollTimelineItem.item.id.eventID else { return } + context.send(viewAction: .edit(pollStartID: pollStartID, poll: pollTimelineItem.item.poll)) + case .end: + guard let pollStartID = pollTimelineItem.item.id.eventID else { return } + context.send(viewAction: .end(pollStartID: pollStartID)) + } + } + } + .padding(.init(top: 12, leading: 12, bottom: 12, trailing: 12)) + .background(.compound.bgCanvasDefaultLevel1) + .cornerRadius(12, corners: .allCorners) + } + } + + private var emptyStateMessage: some View { + Text(context.viewState.emptyStateMessage) + .font(.compound.bodyLG) + .foregroundColor(.compound.textSecondary) + .multilineTextAlignment(.center) + .padding(.vertical, 12) + } + + private var loadMoreButton: some View { + Button { + context.send(viewAction: .loadMore) + } label: { + Text(L10n.Action.loadMore) + .font(.compound.bodyLGSemibold) + .padding(.horizontal, 12) + } + .accessibilityIdentifier(A11yIdentifiers.roomPollsHistoryScreen.loadMore) + .buttonStyle(.compound(.secondary)) + .fixedSize() + .disabled(context.viewState.isBackPaginating) + } +} + +private extension DateFormatter { + static let pollTimestamp: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + return dateFormatter + }() +} + +// MARK: - Previews + +struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview { + static let viewModelEmpty: RoomPollsHistoryScreenViewModel = { + let roomTimelineController = MockRoomTimelineController() + roomTimelineController.timelineItems = [] + let roomProxyMockConfiguration = RoomProxyMockConfiguration(displayName: "Polls") + roomProxyMockConfiguration.timeline.timelineStartReached = false + let viewModel = RoomPollsHistoryScreenViewModel(roomProxy: RoomProxyMock(with: roomProxyMockConfiguration), + pollInteractionHandler: PollInteractionHandlerMock(), + roomTimelineController: roomTimelineController, + userIndicatorController: UserIndicatorControllerMock()) + return viewModel + }() + + static let viewModel: RoomPollsHistoryScreenViewModel = { + let roomTimelineController = MockRoomTimelineController() + + let polls = [PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)), + PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true)] + + roomTimelineController.timelineItems = polls + + for i in 0.. = .init() var actions: AnyPublisher { @@ -73,6 +74,7 @@ class RoomScreenInteractionHandler { self.application = application self.appSettings = appSettings self.analyticsService = analyticsService + pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy) } // MARK: Timeline Item Action Menu @@ -274,9 +276,8 @@ class RoomScreenInteractionHandler { func sendPollResponse(pollStartID: String, optionID: String) { Task { - let sendPollResponseResult = await roomProxy.timeline.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) - analyticsService.trackPollVote() - + let sendPollResponseResult = await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) + switch sendPollResponseResult { case .success: break @@ -288,9 +289,8 @@ class RoomScreenInteractionHandler { func endPoll(pollStartID: String) { Task { - let endPollResult = await roomProxy.timeline.endPoll(pollStartID: pollStartID, - text: "The poll with event id: \(pollStartID) has ended") - analyticsService.trackPollEnd() + let endPollResult = await pollInteractionHandler.endPoll(pollStartID: pollStartID) + switch endPollResult { case .success: break diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift b/ElementX/Sources/Screens/RoomScreen/View/Polls/PollOptionView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift rename to ElementX/Sources/Screens/RoomScreen/View/Polls/PollOptionView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift b/ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift new file mode 100644 index 000000000..13e7664ba --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift @@ -0,0 +1,174 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +enum PollViewAction { + case selectOption(optionID: String) + case edit + case end +} + +struct PollView: View { + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) + + let poll: Poll + let editable: Bool + let actionHandler: (PollViewAction) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + questionView + optionsView + summaryView + toolbarView + } + .frame(maxWidth: 450) + } + + // MARK: - Private + + private var questionView: some View { + HStack(alignment: .top, spacing: 12) { + let asset = poll.hasEnded ? Asset.Images.pollsEnd : Asset.Images.polls + + Image(asset.name) + .resizable() + .scaledFrame(size: 22) + .accessibilityHidden(true) + + Text(poll.question) + .multilineTextAlignment(.leading) + .font(.compound.bodyLGSemibold) + } + } + + private var optionsView: some View { + ForEach(poll.options, id: \.id) { option in + Button { + guard !option.isSelected else { return } + actionHandler(.selectOption(optionID: option.id)) + feedbackGenerator.impactOccurred() + } label: { + PollOptionView(pollOption: option, + showVotes: showVotes, + isFinalResult: poll.hasEnded) + .foregroundColor(progressBarColor(for: option)) + } + .disabled(poll.hasEnded) + } + } + + @ViewBuilder + private var summaryView: some View { + if let summaryText = poll.summaryText { + Text(summaryText) + .font(.compound.bodySM) + .scaledPadding(.leading, showVotes ? 0 : 32) + .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading) + } + } + + @ViewBuilder + private var toolbarView: some View { + if !poll.hasEnded, poll.createdByAccountOwner { + Button { + toolbarAction() + } label: { + Text(editable ? L10n.actionEditPoll : L10n.actionEndPoll) + .lineLimit(2, reservesSpace: false) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textOnSolidPrimary) + .padding(.horizontal, 24) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background { + Capsule() + .foregroundColor(.compound.bgActionPrimaryRest) + } + } + .padding(.top, 8) + } + } + + private func toolbarAction() { + if editable { + actionHandler(.edit) + } else { + actionHandler(.end) + } + } + + private func progressBarColor(for option: Poll.Option) -> Color { + if poll.hasEnded { + return option.isWinning ? .compound.textActionAccent : .compound.textDisabled + } else { + return .compound.textPrimary + } + } + + private var showVotes: Bool { + poll.hasEnded || poll.kind == .disclosed + } +} + +private extension Poll { + var summaryText: String? { + guard !hasEnded else { + return options.first.map { + L10n.commonPollTotalVotes($0.allVotes) + } + } + + switch kind { + case .disclosed: + return options.first.map { + L10n.commonPollTotalVotes($0.allVotes) + } + case .undisclosed: + return L10n.commonPollUndisclosedText + } + } +} + +struct PollView_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + PollView(poll: .disclosed(), editable: false) { _ in } + .padding() + .previewDisplayName("Disclosed") + + PollView(poll: .undisclosed(), editable: false) { _ in } + .padding() + .previewDisplayName("Undisclosed") + + PollView(poll: .endedDisclosed, editable: false) { _ in } + .padding() + .previewDisplayName("Ended, Disclosed") + + PollView(poll: .endedUndisclosed, editable: false) { _ in } + .padding() + .previewDisplayName("Ended, Undisclosed") + + PollView(poll: .disclosed(createdByAccountOwner: true), editable: true) { _ in } + .padding() + .previewDisplayName("Creator, disclosed") + + PollView(poll: .emptyDisclosed, editable: true) { _ in } + .padding() + .previewDisplayName("Creator, no votes") + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift index 0cd6583af..a67e45282 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift @@ -19,136 +19,35 @@ import SwiftUI struct PollRoomTimelineView: View { let timelineItem: PollRoomTimelineItem @EnvironmentObject private var context: RoomScreenViewModel.Context - + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) - + var body: some View { TimelineStyler(timelineItem: timelineItem) { - VStack(alignment: .leading, spacing: 16) { - questionView - optionsView - summaryView - toolbarView + PollView(poll: poll, editable: timelineItem.isEditable) { 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: .poll(.selectOption(pollStartID: eventID, optionID: option.id))) + case .edit: + guard let eventID else { return } + context.send(viewAction: .poll(.edit(pollStartID: eventID, poll: poll))) + case .end: + guard let eventID else { return } + context.send(viewAction: .poll(.end(pollStartID: eventID))) + } } - .frame(maxWidth: 450) - } - } - - // MARK: - Private - - private var poll: Poll { - timelineItem.poll - } - - private var eventID: String? { - timelineItem.id.eventID - } - - private var questionView: some View { - HStack(alignment: .top, spacing: 12) { - let asset = poll.hasEnded ? Asset.Images.pollsEnd : Asset.Images.polls - - Image(asset.name) - .resizable() - .scaledFrame(size: 22) - .accessibilityHidden(true) - - Text(poll.question) - .multilineTextAlignment(.leading) - .font(.compound.bodyLGSemibold) - } - } - - private var optionsView: some View { - ForEach(poll.options, id: \.id) { option in - Button { - guard let eventID, !option.isSelected else { return } - context.send(viewAction: .poll(.selectOption(pollStartID: eventID, optionID: option.id))) - feedbackGenerator.impactOccurred() - } label: { - PollOptionView(pollOption: option, - showVotes: showVotes, - isFinalResult: poll.hasEnded) - .foregroundColor(progressBarColor(for: option)) - } - .disabled(poll.hasEnded || eventID == nil) - } - } - - @ViewBuilder - private var summaryView: some View { - if let summaryText = poll.summaryText { - Text(summaryText) - .font(.compound.bodySM) - .scaledPadding(.leading, showVotes ? 0 : 32) - .foregroundColor(.compound.textSecondary) - .frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading) - } - } - - @ViewBuilder - private var toolbarView: some View { - if !poll.hasEnded, poll.createdByAccountOwner { - Button { - toolbarAction() - } label: { - Text(timelineItem.isEditable ? L10n.actionEditPoll : L10n.actionEndPoll) - .lineLimit(2, reservesSpace: false) - .font(.compound.bodyLGSemibold) - .foregroundColor(.compound.textOnSolidPrimary) - .padding(.horizontal, 24) - .padding(.vertical, 10) - .frame(maxWidth: .infinity) - .background { - Capsule() - .foregroundColor(.compound.bgActionPrimaryRest) - } - } - .padding(.top, 8) } } - private func toolbarAction() { - guard let eventID else { - return - } - - if timelineItem.isEditable { - context.send(viewAction: .poll(.edit(pollStartID: eventID, poll: poll))) - } else { - context.send(viewAction: .poll(.end(pollStartID: eventID))) - } + // MARK: - Private + + private var poll: Poll { + timelineItem.poll } - - private func progressBarColor(for option: Poll.Option) -> Color { - if poll.hasEnded { - return option.isWinning ? .compound.textActionAccent : .compound.textDisabled - } else { - return .compound.textPrimary - } - } - - private var showVotes: Bool { - poll.hasEnded || poll.kind == .disclosed - } -} - -private extension Poll { - var summaryText: String? { - guard !hasEnded else { - return options.first.map { - L10n.commonPollTotalVotes($0.allVotes) - } - } - - switch kind { - case .disclosed: - return options.first.map { - L10n.commonPollTotalVotes($0.allVotes) - } - case .undisclosed: - return L10n.commonPollUndisclosedText - } + + private var eventID: String? { + timelineItem.id.eventID } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index b8639b5f5..daac4feb7 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -74,7 +74,7 @@ struct DeveloperOptionsScreen: View { Text("Use encryption") } } - + Section { Button { showConfetti = true diff --git a/ElementX/Sources/Services/Polls/PollInteractionHandler.swift b/ElementX/Sources/Services/Polls/PollInteractionHandler.swift new file mode 100644 index 000000000..507043af9 --- /dev/null +++ b/ElementX/Sources/Services/Polls/PollInteractionHandler.swift @@ -0,0 +1,41 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class PollInteractionHandler: PollInteractionHandlerProtocol { + let analyticsService: AnalyticsService + let roomProxy: RoomProxyProtocol + + init(analyticsService: AnalyticsService, roomProxy: RoomProxyProtocol) { + self.analyticsService = analyticsService + self.roomProxy = roomProxy + } + + func sendPollResponse(pollStartID: String, optionID: String) async -> Result { + let sendPollResponseResult = await roomProxy.timeline.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) + analyticsService.trackPollVote() + + return sendPollResponseResult.mapError { $0 } + } + + func endPoll(pollStartID: String) async -> Result { + let endPollResult = await roomProxy.timeline.endPoll(pollStartID: pollStartID, + text: "The poll with event id: \(pollStartID) has ended") + analyticsService.trackPollEnd() + return endPollResult.mapError { $0 } + } +} diff --git a/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift b/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift new file mode 100644 index 000000000..9caa2bed1 --- /dev/null +++ b/ElementX/Sources/Services/Polls/PollInteractionHandlerProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol PollInteractionHandlerProtocol { + func sendPollResponse(pollStartID: String, optionID: String) async -> Result + func endPoll(pollStartID: String) async -> Result +} + +// sourcery: AutoMockable +extension PollInteractionHandlerProtocol { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index fa0cc23b9..09ccc4b86 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -31,6 +31,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { let callbacks = PassthroughSubject() var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default + var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:] private var client: UITestsSignalling.Client? @@ -43,7 +44,12 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { fatalError("Failure setting up signalling: \(error)") } } - + + func paginateBackwards(requestSize: UInt) async -> Result { + try? await simulateBackPagination() + return .success(()) + } + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { callbacks.send(.canBackPaginate(false)) return .success(()) @@ -99,6 +105,10 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { await roomProxy?.timeline.cancelSend(transactionID: transactionID) } + func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? { + timelineItemsTimestamp[itemID] ?? .now + } + // MARK: - UI Test signalling /// The cancellable used for UI Tests signalling. @@ -153,6 +163,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { timelineItems.insert(contentsOf: newItems, at: 0) callbacks.send(.updatedTimelineItems) callbacks.send(.isBackPaginating(false)) + callbacks.send(.canBackPaginate(!backPaginationResponses.isEmpty)) try client?.send(.success) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 1464aa85d..c2deb276b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -69,6 +69,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol { NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) } + func paginateBackwards(requestSize: UInt) async -> Result { + MXLog.info("Started back pagination request") + switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize) { + case .success: + MXLog.info("Finished back pagination request") + return .success(()) + case .failure(let error): + MXLog.error("Failed back pagination request with error: \(error)") + return .failure(.generic) + } + } + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { MXLog.info("Started back pagination request") switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) { @@ -236,7 +248,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private func updateTimelineItems() { var newTimelineItems = [RoomTimelineItemProtocol]() - var canBackPaginate = true + var canBackPaginate = !roomProxy.timeline.timelineStartReached var isBackPaginating = false var lastEncryptedHistoryItemIndex: Int? @@ -299,7 +311,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { break } } - + timelineItems = newTimelineItems callbacks.send(.updatedTimelineItems) @@ -384,4 +396,20 @@ class RoomTimelineController: RoomTimelineControllerProtocol { break } } + + func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? { + for itemProxy in roomProxy.timeline.timelineProvider.itemProxies { + switch itemProxy { + case .event(let eventTimelineItemProxy): + if eventTimelineItemProxy.id == itemID { + return eventTimelineItemProxy.timestamp + } + case .virtual: + break + case .unknown: + break + } + } + return nil + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 3ff2a35b4..a49c85e38 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -45,6 +45,8 @@ protocol RoomTimelineControllerProtocol { func processItemDisappearance(_ itemID: TimelineItemIdentifier) async + func paginateBackwards(requestSize: UInt) async -> Result + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result @@ -70,6 +72,8 @@ protocol RoomTimelineControllerProtocol { func retrySending(itemID: TimelineItemIdentifier) async func cancelSending(itemID: TimelineItemIdentifier) async + + func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? } extension RoomTimelineControllerProtocol { diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 0267ba460..4be228f85 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -34,6 +34,8 @@ final class TimelineProxy: TimelineProxyProtocol { private let backPaginationStateSubject = PassthroughSubject() private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>() + + private(set) var timelineStartReached = false private let actionsSubject = PassthroughSubject() var actions: AnyPublisher { @@ -134,6 +136,18 @@ final class TimelineProxy: TimelineProxyProtocol { try? timeline.getTimelineEventContentByEventId(eventId: eventID) } + func paginateBackwards(requestSize: UInt) async -> Result { + do { + try await Task.dispatch(on: .global()) { + try self.timeline.paginateBackwards(opts: .simpleRequest(eventLimit: UInt16(requestSize), waitForToken: true)) + } + + return .success(()) + } catch { + return .failure(.failedPaginatingBackwards) + } + } + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { do { try await Task.dispatch(on: .global()) { @@ -480,6 +494,9 @@ final class TimelineProxy: TimelineProxyProtocol { private func subscribeToBackpagination() { let listener = RoomBackpaginationStatusListener { [weak self] status in + if status == .timelineStartReached { + self?.timelineStartReached = true + } self?.backPaginationStateSubject.send(status) } do { diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index c35687188..40ad7ec81 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -43,6 +43,8 @@ protocol TimelineProxyProtocol { var timelineProvider: RoomTimelineProviderProtocol { get } + var timelineStartReached: Bool { get } + func subscribeForUpdates() async /// Cancels a failed message given its transaction ID from the timeline @@ -62,6 +64,8 @@ protocol TimelineProxyProtocol { /// Retries sending a failed message given its transaction ID func retrySend(transactionID: String) async + func paginateBackwards(requestSize: UInt) async -> Result + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result func sendAudio(url: URL, diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index f109f304a..bdb5f90ac 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -877,6 +877,40 @@ class MockScreen: Identifiable { let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .roomPollsHistoryEmptyLoadMore: + let navigationStackCoordinator = NavigationStackCoordinator() + let interactionHandler = PollInteractionHandlerMock() + let roomTimelineController = MockRoomTimelineController() + roomTimelineController.backPaginationResponses = [ + [], + [] + ] + let roomProxyMockConfiguration = RoomProxyMockConfiguration(displayName: "Polls") + roomProxyMockConfiguration.timeline.timelineStartReached = false + let parameters = RoomPollsHistoryScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: roomProxyMockConfiguration), + pollInteractionHandler: interactionHandler, + roomTimelineController: roomTimelineController) + let coordinator = RoomPollsHistoryScreenCoordinator(parameters: parameters) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .roomPollsHistoryLoadMore: + let navigationStackCoordinator = NavigationStackCoordinator() + let interactionHandler = PollInteractionHandlerMock() + let roomTimelineController = MockRoomTimelineController() + + let poll = PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true) + roomTimelineController.timelineItems = [poll] + let date: Date! = DateComponents(calendar: .current, timeZone: .gmt, year: 2023, month: 12, day: 1, hour: 12).date + roomTimelineController.timelineItemsTimestamp = [poll.id: date] + + let roomProxyMockConfiguration = RoomProxyMockConfiguration(displayName: "Polls") + roomProxyMockConfiguration.timeline.timelineStartReached = false + let parameters = RoomPollsHistoryScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: roomProxyMockConfiguration), + pollInteractionHandler: interactionHandler, + roomTimelineController: roomTimelineController) + let coordinator = RoomPollsHistoryScreenCoordinator(parameters: parameters) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator } }() } diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 168a19081..aa5febcd1 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -86,6 +86,8 @@ enum UITestsScreenIdentifier: String { case createRoom case createRoomNoUsers case createPoll + case roomPollsHistoryEmptyLoadMore + case roomPollsHistoryLoadMore } extension UITestsScreenIdentifier: CustomStringConvertible { diff --git a/UITests/Sources/RoomPollsHistoryScreenUITests.swift b/UITests/Sources/RoomPollsHistoryScreenUITests.swift new file mode 100644 index 000000000..6091e9b3c --- /dev/null +++ b/UITests/Sources/RoomPollsHistoryScreenUITests.swift @@ -0,0 +1,36 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@MainActor +class RoomPollsHistoryScreenUITests: XCTestCase { + func testEmptyPollsHistory() async throws { + let app = Application.launch(.roomPollsHistoryEmptyLoadMore) + + XCTAssert(app.buttons[A11yIdentifiers.roomPollsHistoryScreen.loadMore].waitForExistence(timeout: 1)) + + try await app.assertScreenshot(.roomPollsHistoryEmptyLoadMore) + } + + func testPollsHistory() async throws { + let app = Application.launch(.roomPollsHistoryLoadMore) + + XCTAssert(app.buttons[A11yIdentifiers.roomPollsHistoryScreen.loadMore].waitForExistence(timeout: 1)) + + try await app.assertScreenshot(.roomPollsHistoryLoadMore) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png index 033e26ea6..9d281731c 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0266cfccbe6a640908b54c226e1e2d544bedd1aa3343304163fc87f54c9d437 -size 105010 +oid sha256:e0f0d0153b192c9d39095958380d457e2cf2857fb7de3e385c48dba4b15219d2 +size 108327 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png index 789b439aa..12593ca0f 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:013cb46ed50d1a9c302218afbac4aec5a99c76876915dbe83e54354f444aedd3 -size 134604 +oid sha256:ccb8342762f048022b0cb9b2100d235e120e56e485a7564c0bd572a807398bf2 +size 138647 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png index 50c512789..cb9841c5a 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b01d2cef8bcd3400a276b8e4a4757ab2dd2c431ea00892e7a609319988022db -size 137928 +oid sha256:1c83c28c9e8ee2196c5874fe958f1003aae29156afac721478578dc372706d48 +size 140846 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithInvite.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithInvite.png index 3371a1a9b..33ae9a715 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithInvite.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithInvite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c17e1eb73421a3c3ef948f0c1007673a67130ff126d4eb54833e56b82e3f96a -size 109992 +oid sha256:066a49f7d5f1e8fd46a3f35c403bd1b8257e5b30ad6c3af15ea666a783e1f399 +size 113268 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png index b213ee8f6..a8ebcaabc 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:010e94e5b3d969f7141afb1951fcb6c87dd9ea6a48dc944dfeb8bac3a1e66e74 -size 142139 +oid sha256:337b7463033f2b86866f880461402139586b960333b6f0e628b57eb3b32a0454 +size 145428 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png new file mode 100644 index 000000000..72732833c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f8bb38f2fef834933223d9f2fde4e9739a93e025dafbec63931e4f4e35e7a19 +size 73279 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryLoadMore.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryLoadMore.png new file mode 100644 index 000000000..3d37f0ed7 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPollsHistoryLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2b9a7343b1c186a071eed7a3c337101cc0efb569b612f1b5e0737a84e798355 +size 113574 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png index c91927867..c42139e7b 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6398dc3faa59f76171d5b07d51189b28ea8c5b4842e65b2c6b2d2ef8c9a876fc -size 130901 +oid sha256:9e7814adddbd5a2ff51be824e5a842edd4c7ba7c2fdfdef635a6758e09cbf661 +size 133394 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png index 89f1ef901..9c865795d 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cd5e793eaf7b9d7c49e7a941f80d3cda9006a91035e36fa74f973bd707605a7 -size 178087 +oid sha256:1ff99b080b22b3c09001fc3fc835e3b41c877f5e7f2cd6cfe26ef2f7c01a54c1 +size 176256 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithEmptyTopic.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithEmptyTopic.png index cac8ff631..915b6a66f 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithEmptyTopic.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithEmptyTopic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:879e4c49e9a0e3ea321f30f0fe4e157abb0576ec97d116037b3fbd45425ee4b8 -size 179235 +oid sha256:9988534198d133d1f9e020e6af5628259bea9c6fdda64ac5b168230dc0a4621e +size 176611 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithInvite.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithInvite.png index f67b9d1de..694a1bfe8 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithInvite.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithInvite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ce09743029cf0f0c0538c23dce32014338a186d3b647bbefba1c4eb43d5962b -size 137992 +oid sha256:e91d758ef441d68019b14c2416d761f9f522321f8d1e9d826072eb3fcf07dee6 +size 142550 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithRoomAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithRoomAvatar.png index a1030c473..9d26d5265 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithRoomAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenWithRoomAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:374b2e5744e3006e6cf0424bfbe05126465bd8ef3e1926ab1bd370797bbce441 -size 189541 +oid sha256:2a99249f04ea968b762cf14ff335f048eb7d91374c01761d8262c526f858431e +size 194812 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryEmptyLoadMore.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryEmptyLoadMore.png new file mode 100644 index 000000000..5eb64417b --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryEmptyLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eddf4597e366941142a3df33e8341465b373eac43fb5f285a2a3a39f2d81d3df +size 79393 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryLoadMore.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryLoadMore.png new file mode 100644 index 000000000..6e2263802 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPollsHistoryLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6f01046e054115621ff5ef7189e5bf791555d9729af959c6e38a8a048f4350e +size 136913 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreen.png index f6bd5b5f9..28a94d314 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cccaa927653f0ad0c743af8b3638e9dc05329cee2c9be75b0c538b33ab248e8 -size 119658 +oid sha256:3288dfb3ed775665e37c214c4f2d2033e3174f22f41006f5fe0d1878a60866c1 +size 123312 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png index ca6109b79..629e530cd 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ae457bdca037f41371586172d1ad7856d0e204257f55de0b8ac56e4e1cf0100 -size 149272 +oid sha256:47e87c18eb21430919e24ebc8307531e479e03fefffc001e45623739ea9ec62f +size 154961 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png index 141a90194..da7c29893 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithEmptyTopic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e50c3b2e4ea93ac0eb7ac8fe76ce705c2ad70acf9d45169deb27377d811b3b1 -size 153678 +oid sha256:be36151a47830413040624f7d0922bd389321e11387e72c7581f691b3ea37b94 +size 156406 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithInvite.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithInvite.png index 0749ce4d5..beedbdf6b 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithInvite.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithInvite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2012b95de7998c0321c7f8c7e14a5c336064eab574418f0110007c41098d1d75 -size 125205 +oid sha256:40e4ca832c774e501232f9d4a5dfaceae34d72d3a8f4a0647ff26afa73f5552e +size 129593 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png index abe900c3b..4fbec4371 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenWithRoomAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e1582e788c142a48c58b95109f9e61ea4ca33039457614fe7abf329da331d72 -size 157243 +oid sha256:46290c09fa96860d002308e7fdc80dd9840c3ed12567489343ed568e1d11c55c +size 160957 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png new file mode 100644 index 000000000..47d61d377 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryEmptyLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9606120bc5a84596e5c8fd0fc5ed645327ad2dcb231520cb3359ee6fe3250a4a +size 76919 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryLoadMore.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryLoadMore.png new file mode 100644 index 000000000..67b0852e5 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPollsHistoryLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd4d7f3b5f97247a3dabd79ad1349649b64f075c88cb7e44b79aa77e21930e1b +size 126359 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreen.png index 249545b55..ba64d7bdb 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83076ceeb1fab6a333305fd9810c6a4475ac2779ba0154ef9f94570f6131a4ea -size 165487 +oid sha256:380a2caf2250cba0f7f69763860958ef8d3107d6841b10e139221022f719f7e2 +size 165836 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png index e12add6aa..46b05bf6e 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce6b94d85f64b6687c8cfbe91b25811b3b28ce8ef2350ffc0bb18852be0930e9 -size 206487 +oid sha256:ede47654000517fbf67ef6085f4b9c9574b50a73723925690a868dfad1c4684a +size 207805 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithEmptyTopic.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithEmptyTopic.png index 1806691f4..ef1dcf98c 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithEmptyTopic.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithEmptyTopic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe7064493f7f4838af12b13eb51295ae68163ceae6740bda6dbdbd36705ccc29 -size 194311 +oid sha256:0ef967694784f25e8e3614498d8ada07569910917432815523e34a9a4f79f301 +size 178881 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithInvite.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithInvite.png index 8104673a8..37b738ed7 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithInvite.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithInvite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4502d0e8b1e37f8917226b425be5c6c50d9eaaf6c65d43cd80e6b391ad8e8157 -size 168510 +oid sha256:360f724b77a289e54e99ecb225e33cb30252a23bcae18f7f6de287f550c37b70 +size 171690 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithRoomAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithRoomAvatar.png index dc120b3a3..6181e7fbb 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithRoomAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenWithRoomAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ff50bea189beb7dcfa9efb5d3cfcadbdf5abe5c5a3b7db924719de2ab48e545 -size 214384 +oid sha256:e75114908cf9e7049ceafeda01d5c69eb48a54bef628245a5eecd805e00e8026 +size 201023 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryEmptyLoadMore.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryEmptyLoadMore.png new file mode 100644 index 000000000..abb112a99 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryEmptyLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93857a1d768489bd8854e67520d436c31a73cb861a78509a1255cc92d8ceef5b +size 85920 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryLoadMore.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryLoadMore.png new file mode 100644 index 000000000..ce69047cd --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPollsHistoryLoadMore.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2c163403ced353a3d7871275cf8619d7d9ab8da7c2819282043458f233011ee +size 160495 diff --git a/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift new file mode 100644 index 000000000..33426da26 --- /dev/null +++ b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift @@ -0,0 +1,189 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class RoomPollsHistoryScreenViewModelTests: XCTestCase { + var viewModel: RoomPollsHistoryScreenViewModelProtocol! + var interactionHandler: PollInteractionHandlerMock! + var timelineController: MockRoomTimelineController! + + var context: RoomPollsHistoryScreenViewModelType.Context { + viewModel.context + } + + override func setUpWithError() throws { + interactionHandler = PollInteractionHandlerMock() + timelineController = MockRoomTimelineController() + let roomProxyMockConfiguration = RoomProxyMockConfiguration(displayName: "Polls") + roomProxyMockConfiguration.timeline.timelineStartReached = false + viewModel = RoomPollsHistoryScreenViewModel(roomProxy: RoomProxyMock(with: roomProxyMockConfiguration), + pollInteractionHandler: interactionHandler, + roomTimelineController: timelineController, + userIndicatorController: UserIndicatorControllerMock()) + } + + func testBackPaginate() async throws { + timelineController.backPaginationResponses = [ + [PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)), + PollRoomTimelineItem.mock(poll: .endedDisclosed)] + ] + + let deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.isBackPaginating, transitionValues: [false, true, false]) + + viewModel.context.send(viewAction: .loadMore) + + try await deferredViewState.fulfill() + + XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3) + XCTAssertFalse(viewModel.context.viewState.canBackPaginate) + } + + func testBackPaginateCanBackPaginate() async throws { + timelineController.backPaginationResponses = [ + [PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)), + PollRoomTimelineItem.mock(poll: .endedDisclosed)], + [] + ] + + let deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.isBackPaginating, transitionValues: [false, true, false]) + + viewModel.context.send(viewAction: .loadMore) + + try await deferredViewState.fulfill() + + XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3) + XCTAssert(viewModel.context.viewState.canBackPaginate) + } + + func testBackPaginateTwice() async throws { + timelineController.backPaginationResponses = [ + [PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false))], + [PollRoomTimelineItem.mock(poll: .endedDisclosed)] + ] + let deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.isBackPaginating, transitionValues: [false, true, false]) + + viewModel.context.send(viewAction: .loadMore) + viewModel.context.send(viewAction: .loadMore) + + try await deferredViewState.fulfill() + + XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3) + XCTAssert(viewModel.context.viewState.canBackPaginate) + } + + func testFilters() async throws { + timelineController.backPaginationResponses = [ + [PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)), + PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)), + PollRoomTimelineItem.mock(poll: .endedDisclosed)], + [] + ] + + let deferredViewState = deferFulfillment(viewModel.context.$viewState) { value in + !value.pollTimelineItems.isEmpty + } + + viewModel.context.filter = .ongoing + viewModel.context.send(viewAction: .loadMore) + + try await deferredViewState.fulfill() + + XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 3) + + viewModel.context.send(viewAction: .filter(.past)) + XCTAssertEqual(viewModel.context.viewState.pollTimelineItems.count, 1) + } + + func testEndPoll() async throws { + let deferred = deferFulfillment(interactionHandler.publisher) { _ in true } + + interactionHandler.endPollPollStartIDReturnValue = .success(()) + viewModel.context.send(viewAction: .end(pollStartID: "somePollID")) + + try await deferred.fulfill() + + XCTAssert(interactionHandler.endPollPollStartIDCalled) + XCTAssertEqual(interactionHandler.endPollPollStartIDReceivedPollStartID, "somePollID") + } + + func testEndPollFailure() async throws { + let deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.alertInfo != nil + } + + interactionHandler.endPollPollStartIDReturnValue = .failure(TimelineProxyError.failedEndingPoll) + viewModel.context.send(viewAction: .end(pollStartID: "somePollID")) + + try await deferred.fulfill() + + XCTAssert(interactionHandler.endPollPollStartIDCalled) + XCTAssertEqual(interactionHandler.endPollPollStartIDReceivedPollStartID, "somePollID") + } + + func testSendPollResponse() async throws { + let deferred = deferFulfillment(interactionHandler.publisher) { _ in true } + + interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .success(()) + viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) + + try await deferred.fulfill() + + XCTAssert(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) + XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID, "somePollID") + XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID, "someOptionID") + } + + func testSendPollResponseFailure() async throws { + let deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.alertInfo != nil + } + + interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .failure(TimelineProxyError.failedSendingPollResponse) + viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) + + try await deferred.fulfill() + + XCTAssert(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) + XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID, "somePollID") + XCTAssertEqual(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID, "someOptionID") + } + + func testEditPoll() async throws { + let expectedPoll: Poll = .emptyDisclosed + let expectedPollStartID = "someEventID" + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .editPoll(let pollStartID, let poll): + expectedPollStartID == pollStartID && expectedPoll == poll + } + } + + viewModel.context.send(viewAction: .edit(pollStartID: expectedPollStartID, poll: expectedPoll)) + try await deferred.fulfill() + } +} diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-disclosed.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-disclosed.png new file mode 100644 index 000000000..cf47e2536 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-disclosed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b0c5b464a748ad0862049a76bb4f187c679034cc28898447c5058c4931789ac +size 116006 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-no-votes.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-no-votes.png new file mode 100644 index 000000000..aa858e5aa --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Creator-no-votes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3e3a08b89f04a344b1eeedd42100ea950063a5cd0bab3eb944fc12f9e19951a +size 115364 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Disclosed.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Disclosed.png new file mode 100644 index 000000000..2447166cc --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Disclosed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cda1d8f5b00bce67897298cfd4c8f13449c1a4925648845abea59f0b9ad95156 +size 108965 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Disclosed.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Disclosed.png new file mode 100644 index 000000000..aa2d48dc8 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Disclosed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a22c525adf7ae1a33801dea692ce7774d285e8b1c4430167da71709e990c32 +size 109045 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Undisclosed.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Undisclosed.png new file mode 100644 index 000000000..aa2d48dc8 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Ended-Undisclosed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a22c525adf7ae1a33801dea692ce7774d285e8b1c4430167da71709e990c32 +size 109045 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollView.Undisclosed.png b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Undisclosed.png new file mode 100644 index 000000000..049c8f4a8 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollView.Undisclosed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f8d5def15c593ab91d412d432957ca8fbc73c552dc45d8940d32b6f4d512b02 +size 103783 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.DM-Room.png b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.DM-Room.png index c68665281..27985c748 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.DM-Room.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.DM-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e0f795c81b1de86d8fd339a4a41da77e1ba5708ff7f1d353a84cd72872b85ae -size 170145 +oid sha256:3385ad85711489e26d8a7bc8d4951595fbb860f78b894bbdede811b33da4e875 +size 169187 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Generic-Room.png b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Generic-Room.png index b09de985d..c613cd6ac 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Generic-Room.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Generic-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71a9439c43fa9d524757b86286c97364fb36862f7a5baaf85195d3f6c838e9f4 -size 174322 +oid sha256:df5bf3bdc2a327abc5acff85eacf22da2daea56cd87edb20d166bf63ea6c65c5 +size 173146 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Simple-Room.png b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Simple-Room.png index 6b62a4d7e..68c5406e2 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Simple-Room.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomDetailsScreen.Simple-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:485b9d96a003d14cfcc81a607e1cf22277b7a6daabe14cfaf3ab817dd9ff2040 -size 105327 +oid sha256:1d62c100d777596fbec7d9f7d6ad03983f7963bdab029ed231667bdca5db767e +size 112041 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.No-polls.png b/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.No-polls.png new file mode 100644 index 000000000..d78ee0cf1 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.No-polls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:283f397347f3307f448ca2a271ef6e7ec8a71e697c4929e5b8c3d3464e02cf40 +size 93617 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.polls.png b/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.polls.png new file mode 100644 index 000000000..75c2a6553 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomPollsHistoryScreen.polls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf0718060176eba8372ec98d6d2ceddd12a0f25e25bebda7fec9fa0ea904b482 +size 201120 diff --git a/changelog.d/2230.feature b/changelog.d/2230.feature new file mode 100644 index 000000000..4d27768da --- /dev/null +++ b/changelog.d/2230.feature @@ -0,0 +1 @@ +The poll history can be viewed in the room details.