Polls history (#2244)

This commit is contained in:
Nicolas Mauri 2023-12-18 16:38:39 +01:00 committed by GitHub
parent 4ac0ad51d7
commit 72a84badcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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)
}
}

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

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