Polls history (#2244)

This commit is contained in:
Nicolas Mauri
2023-12-18 16:38:39 +01:00
committed by GitHub
parent 4ac45fc30b
commit 7a66c05319
73 changed files with 1516 additions and 202 deletions

View File

@@ -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 = "<group>"; };
0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
0D147EB979902DBBE452EADC /* RoomPollsHistoryScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenUITests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentionalMentions.swift; sourceTree = "<group>"; };
@@ -1180,6 +1191,7 @@
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyMock.swift; sourceTree = "<group>"; };
24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModel.swift; sourceTree = "<group>"; };
24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandlerProtocol.swift; sourceTree = "<group>"; };
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; };
25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = "<group>"; };
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
@@ -1216,6 +1228,7 @@
309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = "<group>"; };
30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = "<group>"; };
314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = "<group>"; };
317F41A4B5C4F457AF710666 /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = "<group>"; };
31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = "<group>"; };
31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = "<group>"; };
31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1280,6 +1293,7 @@
422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = "<group>"; };
42ADEA322D2089391E049535 /* InvitesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreen.swift; sourceTree = "<group>"; };
42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = "<group>"; };
42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = "<group>"; };
42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = "<group>"; };
43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = "<group>"; };
4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = "<group>"; };
@@ -1330,6 +1344,7 @@
505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; };
5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; };
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = "<group>"; };
50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelTests.swift; sourceTree = "<group>"; };
514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkCoordinator.swift; sourceTree = "<group>"; };
51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenModels.swift; sourceTree = "<group>"; };
@@ -1405,7 +1420,6 @@
66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenUITests.swift; sourceTree = "<group>"; };
669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = "<group>"; };
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; };
67779D9A1B797285A09B7720 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = "<group>"; };
6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; };
693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
@@ -1429,6 +1443,7 @@
6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.swift; sourceTree = "<group>"; };
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = "<group>"; };
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = "<group>"; };
6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = "<group>"; };
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1590,6 +1605,7 @@
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
A0A01AECFF54281CF35909A6 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; };
A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = "<group>"; };
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = "<group>"; };
@@ -1657,6 +1673,7 @@
B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = "<group>"; };
B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = "<group>"; };
B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelTests.swift; sourceTree = "<group>"; };
B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = "<group>"; };
B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = "<group>"; };
B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = "<group>"; };
@@ -1778,6 +1795,7 @@
D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = "<group>"; };
D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = "<group>"; };
D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenCoordinator.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = "<group>"; };
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = "<group>"; };
@@ -1817,6 +1835,7 @@
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = "<group>"; };
DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = "<group>"; };
DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = "<group>"; };
DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenUITests.swift; sourceTree = "<group>"; };
@@ -1885,6 +1904,7 @@
F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelTests.swift; sourceTree = "<group>"; };
F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyle.swift; sourceTree = "<group>"; };
F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModel.swift; sourceTree = "<group>"; };
F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = "<group>"; };
F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
45778D52AECD4EB99A289214 /* Polls */ = {
isa = PBXGroup;
children = (
50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */,
317F41A4B5C4F457AF710666 /* PollView.swift */,
);
path = Polls;
sourceTree = "<group>";
};
4775A7D6FBB210BF21318AD9 /* UserDetailsEditScreen */ = {
isa = PBXGroup;
children = (
@@ -2974,6 +3004,15 @@
path = ReportContentScreen;
sourceTree = "<group>";
};
599DFFE0805B08454E40D64A /* Polls */ = {
isa = PBXGroup;
children = (
DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */,
259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */,
);
path = Polls;
sourceTree = "<group>";
};
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 = "<group>";
};
D4B487C81A239A9C71807601 /* View */ = {
isa = PBXGroup;
children = (
42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
D4DB8163C10389C069458252 /* RoomMemberListScreen */ = {
isa = PBXGroup;
children = (
@@ -4326,6 +4375,18 @@
path = View;
sourceTree = "<group>";
};
D57B3BC211BB74420C9138D7 /* RoomPollsHistoryScreen */ = {
isa = PBXGroup;
children = (
D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */,
6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */,
F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */,
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */,
D4B487C81A239A9C71807601 /* View */,
);
path = RoomPollsHistoryScreen;
sourceTree = "<group>";
};
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 */,

View File

@@ -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";

View File

@@ -264,7 +264,7 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.chatBackupEnabled, defaultValue: false, storageType: .userDefaults(store))
var chatBackupEnabled
#endif
// MARK: - Shared

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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<Void, Error>!
var sendPollResponsePollStartIDOptionIDClosure: ((String, String) async -> Result<Void, Error>)?
func sendPollResponse(pollStartID: String, optionID: String) async -> Result<Void, Error> {
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<Void, Error>!
var endPollPollStartIDClosure: ((String) async -> Result<Void, Error>)?
func endPoll(pollStartID: String) async -> Result<Void, Error> {
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<Void, TimelineProxyError>!
var paginateBackwardsRequestSizeClosure: ((UInt) async -> Result<Void, TimelineProxyError>)?
func paginateBackwards(requestSize: UInt) async -> Result<Void, TimelineProxyError> {
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

View File

@@ -35,6 +35,7 @@ struct RoomProxyMockConfiguration {
var timeline = {
let mock = TimelineProxyMock()
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
mock.timelineStartReached = false
return mock
}()

View File

@@ -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"
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<RoomPollsHistoryScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<RoomPollsHistoryScreenCoordinatorAction, Never> {
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))
}
}

View File

@@ -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<RoomPollsHistoryScreenErrorType>?
}
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
}

View File

@@ -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<RoomPollsHistoryScreenViewState, RoomPollsHistoryScreenViewAction>
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<Void, Never>?
private let isPaginatingIndicatorID = UUID().uuidString
private var actionsSubject: PassthroughSubject<RoomPollsHistoryScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<RoomPollsHistoryScreenViewModelAction, Never> {
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())
}

View File

@@ -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<RoomPollsHistoryScreenViewModelAction, Never> { get }
var context: RoomPollsHistoryScreenViewModelType.Context { get }
}

View File

@@ -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..<polls.count {
let item = polls[i]
let date: Date! = DateComponents(calendar: .current, timeZone: .gmt, year: 2023, month: 12, day: 1 + i, hour: 12).date
roomTimelineController.timelineItemsTimestamp[item.id] = date
}
let roomProxyMockConfiguration = RoomProxyMockConfiguration(displayName: "Polls")
roomProxyMockConfiguration.timeline.timelineStartReached = true
let viewModel = RoomPollsHistoryScreenViewModel(roomProxy: RoomProxyMock(with: roomProxyMockConfiguration),
pollInteractionHandler: PollInteractionHandlerMock(),
roomTimelineController: roomTimelineController,
userIndicatorController: UserIndicatorControllerMock())
return viewModel
}()
static var previews: some View {
NavigationStack {
RoomPollsHistoryScreen(context: viewModelEmpty.context)
}
.previewDisplayName("No polls")
.snapshot(delay: 1.0)
NavigationStack {
RoomPollsHistoryScreen(context: viewModel.context)
}
.previewDisplayName("polls")
.snapshot(delay: 1.0)
}
}

View File

@@ -43,6 +43,7 @@ class RoomScreenInteractionHandler {
private let application: ApplicationProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let pollInteractionHandler: PollInteractionHandlerProtocol
private let actionsSubject: PassthroughSubject<RoomScreenInteractionHandlerAction, Never> = .init()
var actions: AnyPublisher<RoomScreenInteractionHandlerAction, Never> {
@@ -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

View File

@@ -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")
}
}

View File

@@ -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
}
}

View File

@@ -74,7 +74,7 @@ struct DeveloperOptionsScreen: View {
Text("Use encryption")
}
}
Section {
Button {
showConfetti = true

View File

@@ -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<Void, Error> {
let sendPollResponseResult = await roomProxy.timeline.sendPollResponse(pollStartID: pollStartID, answers: [optionID])
analyticsService.trackPollVote()
return sendPollResponseResult.mapError { $0 }
}
func endPoll(pollStartID: String) async -> Result<Void, Error> {
let endPollResult = await roomProxy.timeline.endPoll(pollStartID: pollStartID,
text: "The poll with event id: \(pollStartID) has ended")
analyticsService.trackPollEnd()
return endPollResult.mapError { $0 }
}
}

View File

@@ -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<Void, Error>
func endPoll(pollStartID: String) async -> Result<Void, Error>
}
// sourcery: AutoMockable
extension PollInteractionHandlerProtocol { }

View File

@@ -31,6 +31,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
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<Void, RoomTimelineControllerError> {
try? await simulateBackPagination()
return .success(())
}
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> {
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)
}

View File

@@ -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<Void, RoomTimelineControllerError> {
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<Void, RoomTimelineControllerError> {
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
}
}

View File

@@ -45,6 +45,8 @@ protocol RoomTimelineControllerProtocol {
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async
func paginateBackwards(requestSize: UInt) async -> Result<Void, RoomTimelineControllerError>
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError>
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError>
@@ -70,6 +72,8 @@ protocol RoomTimelineControllerProtocol {
func retrySending(itemID: TimelineItemIdentifier) async
func cancelSending(itemID: TimelineItemIdentifier) async
func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date?
}
extension RoomTimelineControllerProtocol {

View File

@@ -34,6 +34,8 @@ final class TimelineProxy: TimelineProxyProtocol {
private let backPaginationStateSubject = PassthroughSubject<BackPaginationStatus, Never>()
private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>()
private(set) var timelineStartReached = false
private let actionsSubject = PassthroughSubject<TimelineProxyAction, Never>()
var actions: AnyPublisher<TimelineProxyAction, Never> {
@@ -134,6 +136,18 @@ final class TimelineProxy: TimelineProxyProtocol {
try? timeline.getTimelineEventContentByEventId(eventId: eventID)
}
func paginateBackwards(requestSize: UInt) async -> Result<Void, TimelineProxyError> {
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<Void, TimelineProxyError> {
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 {

View File

@@ -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<Void, TimelineProxyError>
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError>
func sendAudio(url: URL,

View File

@@ -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
}
}()
}

View File

@@ -86,6 +86,8 @@ enum UITestsScreenIdentifier: String {
case createRoom
case createRoomNoUsers
case createPoll
case roomPollsHistoryEmptyLoadMore
case roomPollsHistoryLoadMore
}
extension UITestsScreenIdentifier: CustomStringConvertible {

View File

@@ -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)
}
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0266cfccbe6a640908b54c226e1e2d544bedd1aa3343304163fc87f54c9d437
size 105010
oid sha256:e0f0d0153b192c9d39095958380d457e2cf2857fb7de3e385c48dba4b15219d2
size 108327

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:013cb46ed50d1a9c302218afbac4aec5a99c76876915dbe83e54354f444aedd3
size 134604
oid sha256:ccb8342762f048022b0cb9b2100d235e120e56e485a7564c0bd572a807398bf2
size 138647

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b01d2cef8bcd3400a276b8e4a4757ab2dd2c431ea00892e7a609319988022db
size 137928
oid sha256:1c83c28c9e8ee2196c5874fe958f1003aae29156afac721478578dc372706d48
size 140846

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c17e1eb73421a3c3ef948f0c1007673a67130ff126d4eb54833e56b82e3f96a
size 109992
oid sha256:066a49f7d5f1e8fd46a3f35c403bd1b8257e5b30ad6c3af15ea666a783e1f399
size 113268

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:010e94e5b3d969f7141afb1951fcb6c87dd9ea6a48dc944dfeb8bac3a1e66e74
size 142139
oid sha256:337b7463033f2b86866f880461402139586b960333b6f0e628b57eb3b32a0454
size 145428

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f8bb38f2fef834933223d9f2fde4e9739a93e025dafbec63931e4f4e35e7a19
size 73279

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2b9a7343b1c186a071eed7a3c337101cc0efb569b612f1b5e0737a84e798355
size 113574

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6398dc3faa59f76171d5b07d51189b28ea8c5b4842e65b2c6b2d2ef8c9a876fc
size 130901
oid sha256:9e7814adddbd5a2ff51be824e5a842edd4c7ba7c2fdfdef635a6758e09cbf661
size 133394

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8cd5e793eaf7b9d7c49e7a941f80d3cda9006a91035e36fa74f973bd707605a7
size 178087
oid sha256:1ff99b080b22b3c09001fc3fc835e3b41c877f5e7f2cd6cfe26ef2f7c01a54c1
size 176256

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:879e4c49e9a0e3ea321f30f0fe4e157abb0576ec97d116037b3fbd45425ee4b8
size 179235
oid sha256:9988534198d133d1f9e020e6af5628259bea9c6fdda64ac5b168230dc0a4621e
size 176611

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ce09743029cf0f0c0538c23dce32014338a186d3b647bbefba1c4eb43d5962b
size 137992
oid sha256:e91d758ef441d68019b14c2416d761f9f522321f8d1e9d826072eb3fcf07dee6
size 142550

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:374b2e5744e3006e6cf0424bfbe05126465bd8ef3e1926ab1bd370797bbce441
size 189541
oid sha256:2a99249f04ea968b762cf14ff335f048eb7d91374c01761d8262c526f858431e
size 194812

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eddf4597e366941142a3df33e8341465b373eac43fb5f285a2a3a39f2d81d3df
size 79393

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6f01046e054115621ff5ef7189e5bf791555d9729af959c6e38a8a048f4350e
size 136913

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cccaa927653f0ad0c743af8b3638e9dc05329cee2c9be75b0c538b33ab248e8
size 119658
oid sha256:3288dfb3ed775665e37c214c4f2d2033e3174f22f41006f5fe0d1878a60866c1
size 123312

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ae457bdca037f41371586172d1ad7856d0e204257f55de0b8ac56e4e1cf0100
size 149272
oid sha256:47e87c18eb21430919e24ebc8307531e479e03fefffc001e45623739ea9ec62f
size 154961

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e50c3b2e4ea93ac0eb7ac8fe76ce705c2ad70acf9d45169deb27377d811b3b1
size 153678
oid sha256:be36151a47830413040624f7d0922bd389321e11387e72c7581f691b3ea37b94
size 156406

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2012b95de7998c0321c7f8c7e14a5c336064eab574418f0110007c41098d1d75
size 125205
oid sha256:40e4ca832c774e501232f9d4a5dfaceae34d72d3a8f4a0647ff26afa73f5552e
size 129593

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e1582e788c142a48c58b95109f9e61ea4ca33039457614fe7abf329da331d72
size 157243
oid sha256:46290c09fa96860d002308e7fdc80dd9840c3ed12567489343ed568e1d11c55c
size 160957

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9606120bc5a84596e5c8fd0fc5ed645327ad2dcb231520cb3359ee6fe3250a4a
size 76919

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd4d7f3b5f97247a3dabd79ad1349649b64f075c88cb7e44b79aa77e21930e1b
size 126359

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83076ceeb1fab6a333305fd9810c6a4475ac2779ba0154ef9f94570f6131a4ea
size 165487
oid sha256:380a2caf2250cba0f7f69763860958ef8d3107d6841b10e139221022f719f7e2
size 165836

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce6b94d85f64b6687c8cfbe91b25811b3b28ce8ef2350ffc0bb18852be0930e9
size 206487
oid sha256:ede47654000517fbf67ef6085f4b9c9574b50a73723925690a868dfad1c4684a
size 207805

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fe7064493f7f4838af12b13eb51295ae68163ceae6740bda6dbdbd36705ccc29
size 194311
oid sha256:0ef967694784f25e8e3614498d8ada07569910917432815523e34a9a4f79f301
size 178881

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4502d0e8b1e37f8917226b425be5c6c50d9eaaf6c65d43cd80e6b391ad8e8157
size 168510
oid sha256:360f724b77a289e54e99ecb225e33cb30252a23bcae18f7f6de287f550c37b70
size 171690

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ff50bea189beb7dcfa9efb5d3cfcadbdf5abe5c5a3b7db924719de2ab48e545
size 214384
oid sha256:e75114908cf9e7049ceafeda01d5c69eb48a54bef628245a5eecd805e00e8026
size 201023

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93857a1d768489bd8854e67520d436c31a73cb861a78509a1255cc92d8ceef5b
size 85920

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2c163403ced353a3d7871275cf8619d7d9ab8da7c2819282043458f233011ee
size 160495

View File

@@ -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()
}
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b0c5b464a748ad0862049a76bb4f187c679034cc28898447c5058c4931789ac
size 116006

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3e3a08b89f04a344b1eeedd42100ea950063a5cd0bab3eb944fc12f9e19951a
size 115364

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cda1d8f5b00bce67897298cfd4c8f13449c1a4925648845abea59f0b9ad95156
size 108965

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72a22c525adf7ae1a33801dea692ce7774d285e8b1c4430167da71709e990c32
size 109045

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72a22c525adf7ae1a33801dea692ce7774d285e8b1c4430167da71709e990c32
size 109045

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f8d5def15c593ab91d412d432957ca8fbc73c552dc45d8940d32b6f4d512b02
size 103783

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e0f795c81b1de86d8fd339a4a41da77e1ba5708ff7f1d353a84cd72872b85ae
size 170145
oid sha256:3385ad85711489e26d8a7bc8d4951595fbb860f78b894bbdede811b33da4e875
size 169187

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71a9439c43fa9d524757b86286c97364fb36862f7a5baaf85195d3f6c838e9f4
size 174322
oid sha256:df5bf3bdc2a327abc5acff85eacf22da2daea56cd87edb20d166bf63ea6c65c5
size 173146

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:485b9d96a003d14cfcc81a607e1cf22277b7a6daabe14cfaf3ab817dd9ff2040
size 105327
oid sha256:1d62c100d777596fbec7d9f7d6ad03983f7963bdab029ed231667bdca5db767e
size 112041

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:283f397347f3307f448ca2a271ef6e7ec8a71e697c4929e5b8c3d3464e02cf40
size 93617

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf0718060176eba8372ec98d6d2ceddd12a0f25e25bebda7fec9fa0ea904b482
size 201120

1
changelog.d/2230.feature Normal file
View File

@@ -0,0 +1 @@
The poll history can be viewed in the room details.