diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 2932ddf7a..0bc56f903 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */; }; 0C88044649BAEE6C49BFC43A /* SecureBackupControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */; }; 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */; }; + 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */; }; 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */; }; 0DCDF49AB95F75BFC8B1879C /* SwipeToReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */; }; 0E08BB72B2258652CF501A8B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 9B68DE8678BF67D4612BCC16 /* Prefire */; }; @@ -138,6 +139,7 @@ 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */; }; 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */; }; 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; + 260FFC1475EE94F641C3F3F9 /* PollFormScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */; }; 266C4DF893F2947DCCEF327B /* InvitesScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC14E5209C262530E19BC4C1 /* InvitesScreenViewModelTests.swift */; }; 2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */; }; 273AB64B9A26B61C51858867 /* AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A07BAEDD74C48795A996A /* AsyncSequence.swift */; }; @@ -227,6 +229,7 @@ 3C549A0BF39F8A854D45D9FD /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; }; 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; + 3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */; }; 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */; }; 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893777A4997BBDB68079D4F5 /* ArrayTests.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; @@ -263,7 +266,6 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; 46C9F8FE3810A04A005FE16B /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */; }; - 46D1E2940ED8CCBF62FE8854 /* CreatePollScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */; }; 4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; @@ -290,7 +292,6 @@ 4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; - 4EB1B717C1EFE3A7ABFBC0A8 /* CreatePollScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A22A3A4109959414EBC6113 /* CreatePollScreenViewModelProtocol.swift */; }; 4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; }; 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; @@ -347,7 +348,6 @@ 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; - 5E415EF9A5D31B1690CE27F5 /* CreatePollScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5DDF245FA51CF75F89E58A4 /* CreatePollScreenUITests.swift */; }; 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.swift */; }; 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F28C9146694B381BB82E18C /* AnalyticsPromptScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B65A314DF40B6BBF775C2BC /* AnalyticsPromptScreenCoordinator.swift */; }; @@ -421,6 +421,7 @@ 70558528EF68CAAEF09972D5 /* RoomTimelineItemFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */; }; 706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; 706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; }; + 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */; }; 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; @@ -430,7 +431,6 @@ 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; }; - 744114780862F0BD1A2D57D6 /* CreatePollScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */; }; 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; }; 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; }; 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; }; @@ -673,7 +673,6 @@ AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */; }; AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; AFA1F2543DFF7B45DF68ACD6 /* CompletionSuggestionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170BF6F7923A5C3792442F27 /* CompletionSuggestionModels.swift */; }; - AFC518DCC38B821537EBF549 /* CreatePollScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */; }; B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */; }; B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; B09DC6E3D0EE87C4D4ABFAB3 /* EncryptedHistoryRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */; }; @@ -713,6 +712,7 @@ B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; }; B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; + B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; }; B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; }; B828C600A54B2EE20871A451 /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD700E035C85738EE4B97129 /* PerformanceTests.swift */; }; B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; }; @@ -838,7 +838,6 @@ D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D8385A51A3D0FA9283556281 /* RoundedLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 745323FCF9AF21A117252C53 /* RoundedLabelItem.swift */; }; - D84D5BDFB1B915389AC807B4 /* CreatePollScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A328F9E556F5CFA89332017 /* CreatePollScreenViewModel.swift */; }; D871C8CF46950F959C9A62C3 /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54464351F170D570110AFCA /* WelcomeScreen.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; @@ -908,7 +907,6 @@ EBDB339A7C127F068B6E52E5 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A634D8DD1E10D858CF7995D /* VoiceMessageRecordingView.swift */; }; EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; EC280623A42904341363EAAF /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = A20EA00CCB9DBE0FFB17DD09 /* Collections */; }; - EC658A57E715699C52DFBC77 /* CreatePollScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB1D0C69FEDD93404DF927E /* CreatePollScreenViewModelTests.swift */; }; ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; ED564C8C7C43CF5F67000368 /* PlatformViewVersionPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */; }; EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; }; @@ -962,6 +960,7 @@ F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; }; F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; + F9EA79092C18A8CFE4922DD2 /* PollFormScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */; }; FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; }; FA4296218444C48BC890F46B /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */; }; FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; @@ -983,6 +982,7 @@ FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; + FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */; }; FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; }; /* End PBXBuildFile section */ @@ -1177,6 +1177,7 @@ 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModel.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; + 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; @@ -1186,7 +1187,6 @@ 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = ""; }; 27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenModels.swift; sourceTree = ""; }; - 27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreen.swift; sourceTree = ""; }; 28146817C61423CACCF942F5 /* CallScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenModels.swift; sourceTree = ""; }; 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheTests.swift; sourceTree = ""; }; 287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; @@ -1219,10 +1219,12 @@ 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; 33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCoordinator.swift; sourceTree = ""; }; + 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenUITests.swift; sourceTree = ""; }; 33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeocoding.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelProtocol.swift; sourceTree = ""; }; 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelProtocol.swift; sourceTree = ""; }; + 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModelTests.swift; sourceTree = ""; }; 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -1241,7 +1243,6 @@ 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenUITests.swift; sourceTree = ""; }; 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerMock.swift; sourceTree = ""; }; - 3A328F9E556F5CFA89332017 /* CreatePollScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenViewModel.swift; sourceTree = ""; }; 3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = ""; }; 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenUITests.swift; sourceTree = ""; }; 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; @@ -1260,7 +1261,6 @@ 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; - 3EB1D0C69FEDD93404DF927E /* CreatePollScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenViewModelTests.swift; sourceTree = ""; }; 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = ""; }; 3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = ""; }; 3F684BDD23ECEADB3053BA5A /* DeveloperOptionsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenUITests.swift; sourceTree = ""; }; @@ -1307,7 +1307,6 @@ 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; - 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; @@ -1407,7 +1406,6 @@ 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListViewModelTests.swift; sourceTree = ""; }; 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = ""; }; - 6A22A3A4109959414EBC6113 /* CreatePollScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenViewModelProtocol.swift; sourceTree = ""; }; 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = ""; }; @@ -1597,12 +1595,12 @@ A3DF0BFE5637EA42F5651FE8 /* MigrationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenCoordinator.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; + A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModelProtocol.swift; sourceTree = ""; }; A433BE28B40D418237BE37B5 /* ReportContentScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreen.swift; sourceTree = ""; }; A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = ""; }; A58DB8EFB91BE920762025D0 /* NCE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NCE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = ""; }; - A5DDF245FA51CF75F89E58A4 /* CreatePollScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenUITests.swift; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; @@ -1687,7 +1685,6 @@ BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB0A77874B29D79DDFC051AC /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = ""; }; - BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenCoordinator.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = ""; }; @@ -1800,6 +1797,7 @@ D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; D5E26C54362206BBDD096D83 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceTests.swift; sourceTree = ""; }; + D622EC7898469BB1D0881CDD /* PollFormScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreen.swift; sourceTree = ""; }; D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; @@ -1864,6 +1862,7 @@ E992D7B8BE54B2AB454613AF /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = ""; }; + EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenModels.swift; sourceTree = ""; }; EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelProtocol.swift; sourceTree = ""; }; EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; @@ -1901,6 +1900,7 @@ F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorToastView.swift; sourceTree = ""; }; F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyMock.swift; sourceTree = ""; }; F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderProtocol.swift; sourceTree = ""; }; + F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModel.swift; sourceTree = ""; }; F6D698BFD68B061350553930 /* WaitingDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingDialog.swift; sourceTree = ""; }; F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockUITests.swift; sourceTree = ""; }; @@ -3235,7 +3235,6 @@ CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */, D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */, CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */, - 3EB1D0C69FEDD93404DF927E /* CreatePollScreenViewModelTests.swift */, 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */, 3B5E97E9615A158C76B2AB77 /* DateTests.swift */, 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */, @@ -3266,6 +3265,7 @@ D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */, + 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */, @@ -3638,10 +3638,10 @@ 90DC2E28718955ED87AD1456 /* CreatePollScreen */ = { isa = PBXGroup; children = ( - BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */, - 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */, - 3A328F9E556F5CFA89332017 /* CreatePollScreenViewModel.swift */, - 6A22A3A4109959414EBC6113 /* CreatePollScreenViewModelProtocol.swift */, + 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */, + EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */, + F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */, + A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */, D57A6F3FC292425BEBDF58BF /* View */, ); path = CreatePollScreen; @@ -3722,7 +3722,6 @@ 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */, - A5DDF245FA51CF75F89E58A4 /* CreatePollScreenUITests.swift */, F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */, 3F684BDD23ECEADB3053BA5A /* DeveloperOptionsScreenUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, @@ -3736,6 +3735,7 @@ 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */, B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */, 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */, + 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */, 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */, 122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */, 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */, @@ -4353,7 +4353,7 @@ D57A6F3FC292425BEBDF58BF /* View */ = { isa = PBXGroup; children = ( - 27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */, + D622EC7898469BB1D0881CDD /* PollFormScreen.swift */, ); path = View; sourceTree = ""; @@ -5185,7 +5185,6 @@ B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */, 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */, 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */, - EC658A57E715699C52DFBC77 /* CreatePollScreenViewModelTests.swift in Sources */, D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */, CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */, 864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */, @@ -5226,6 +5225,7 @@ 50381244BA280451771BE3ED /* PINTextFieldTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */, + FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */, @@ -5416,11 +5416,6 @@ EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */, AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */, C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */, - 46D1E2940ED8CCBF62FE8854 /* CreatePollScreen.swift in Sources */, - 744114780862F0BD1A2D57D6 /* CreatePollScreenCoordinator.swift in Sources */, - AFC518DCC38B821537EBF549 /* CreatePollScreenModels.swift in Sources */, - D84D5BDFB1B915389AC807B4 /* CreatePollScreenViewModel.swift in Sources */, - 4EB1B717C1EFE3A7ABFBC0A8 /* CreatePollScreenViewModelProtocol.swift in Sources */, 564BF06B3E93D6DD55F903B2 /* CreateRoomCoordinator.swift in Sources */, C32765D740C81AD4C42E8F50 /* CreateRoomFlowParameters.swift in Sources */, FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */, @@ -5662,6 +5657,11 @@ 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */, EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */, ED564C8C7C43CF5F67000368 /* PlatformViewVersionPredicate.swift in Sources */, + B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */, + 3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */, + 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */, + F9EA79092C18A8CFE4922DD2 /* PollFormScreenViewModel.swift in Sources */, + 260FFC1475EE94F641C3F3F9 /* PollFormScreenViewModelProtocol.swift in Sources */, 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */, 6B4BF4A6450F55939B49FAEF /* PollOptionView.swift in Sources */, 864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */, @@ -5975,7 +5975,6 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 04778AA4D6AD2E153D7AAFF2 /* CallScreenUITests.swift in Sources */, - 5E415EF9A5D31B1690CE27F5 /* CreatePollScreenUITests.swift in Sources */, 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */, C1F863E16BDBC87255D23B57 /* DeveloperOptionsScreenUITests.swift in Sources */, 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, @@ -5989,6 +5988,7 @@ 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */, AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */, 92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */, + 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */, BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */, F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */, 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */, @@ -6652,7 +6652,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "0.0.4-november23"; + version = "0.0.5-november23"; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f9f266a7c..df506b209 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -130,8 +130,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "5ea5fd66ee766cec2870441265625aded58507a8", - "version" : "0.0.4-november23" + "revision" : "415201caf63d5338c6007d4ed61978011a95bcb1", + "version" : "0.0.5-november23" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index cf65b9ce8..1985bcb70 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -360,8 +360,8 @@ "screen_create_poll_anonymous_desc" = "Show results only after poll ends"; "screen_create_poll_anonymous_headline" = "Hide votes"; "screen_create_poll_answer_hint" = "Option %1$d"; -"screen_create_poll_discard_confirmation" = "Are you sure you want to discard this poll?"; -"screen_create_poll_discard_confirmation_title" = "Discard Poll"; +"screen_create_poll_discard_confirmation" = "Your changes wonโ€™t be saved"; +"screen_create_poll_discard_confirmation_title" = "Cancel Poll"; "screen_create_poll_question_desc" = "Question or topic"; "screen_create_poll_question_hint" = "What is the poll about?"; "screen_create_poll_title" = "Create Poll"; @@ -375,6 +375,8 @@ "screen_create_room_public_option_title" = "Public room (anyone)"; "screen_create_room_room_name_label" = "Room name"; "screen_create_room_topic_label" = "Topic (optional)"; +"screen_edit_poll_delete_confirmation" = "Are you sure you want to delete this poll?"; +"screen_edit_poll_delete_confirmation_title" = "Delete Poll"; "screen_edit_poll_title" = "Edit poll"; "screen_edit_profile_display_name" = "Display name"; "screen_edit_profile_display_name_placeholder" = "Your display name"; diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index c95071b02..0057b577b 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -182,9 +182,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.dismissNotificationSettingsScreen, .notificationSettingsScreen(let roomID)): return .roomDetails(roomID: roomID, isRoot: false) - case (.presentCreatePollForm, .room(let roomID)): - return .createPollForm(roomID: roomID) - case (.dismissCreatePollForm, .createPollForm(let roomID)): + case (.presentPollForm, .room(let roomID)): + return .pollForm(roomID: roomID) + case (.dismissPollForm, .pollForm(let roomID)): return .room(roomID: roomID) default: @@ -260,9 +260,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.notificationSettingsScreen, .dismissNotificationSettingsScreen, .roomDetails): break - case (.room, .presentCreatePollForm, .createPollForm): - presentCreatePollForm() - case (.createPollForm, .dismissCreatePollForm, .room): + case (.room, .presentPollForm(let mode), .pollForm): + presentPollForm(mode: mode) + case (.pollForm, .dismissPollForm, .room): break default: @@ -374,8 +374,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) case .presentLocationPicker: stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker)) - case .presentPollForm: - stateMachine.tryEvent(.presentCreatePollForm) + case .presentPollForm(let mode): + stateMachine.tryEvent(.presentPollForm(mode: mode)) case .presentLocationViewer(_, let geoURI, let description): stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description))) case .presentRoomMemberDetails(member: let member): @@ -631,9 +631,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentCreatePollForm() { + private func presentPollForm(mode: PollFormMode) { let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = CreatePollScreenCoordinator(parameters: .init()) + let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode)) navigationStackCoordinator.setRootCoordinator(coordinator) coordinator.actions @@ -647,39 +647,86 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch action { case .cancel: break - case let .create(question, options, pollKind): - Task { - guard let roomProxy = self.roomProxy else { - self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) - return - } - - let result = await roomProxy.createPoll(question: question, answers: options, pollKind: pollKind) - - self.analytics.trackComposer(inThread: false, - isEditing: false, - isReply: false, - messageType: .poll, - startsThread: nil) - - self.analytics.trackPollCreated(isUndisclosed: pollKind == .undisclosed, numberOfAnswers: options.count) - - switch result { - case .success: - break - case .failure: - self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) - } + case .delete: + deletePoll(mode: mode) + case let .submit(question, options, pollKind): + switch mode { + case .new: + createPoll(question: question, options: options, pollKind: pollKind) + case .edit(let eventID, _): + editPoll(pollStartID: eventID, question: question, options: options, pollKind: pollKind) } } } .store(in: &cancellables) navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator) { [weak self] in - self?.stateMachine.tryEvent(.dismissCreatePollForm) + self?.stateMachine.tryEvent(.dismissPollForm) } } + + private func createPoll(question: String, options: [String], pollKind: Poll.Kind) { + Task { + guard let roomProxy = self.roomProxy else { + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + return + } + let result = await roomProxy.createPoll(question: question, answers: options, pollKind: pollKind) + + self.analytics.trackComposer(inThread: false, + isEditing: false, + isReply: false, + messageType: .poll, + startsThread: nil) + + self.analytics.trackPollCreated(isUndisclosed: pollKind == .undisclosed, numberOfAnswers: options.count) + + switch result { + case .success: + break + case .failure: + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + } + } + } + + private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind) { + Task { + guard let roomProxy = self.roomProxy else { + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + return + } + + let result = await roomProxy.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) + + switch result { + case .success: + break + case .failure: + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + } + } + } + + private func deletePoll(mode: PollFormMode) { + Task { + guard case .edit(let pollStartID, _) = mode, let roomProxy = self.roomProxy else { + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + return + } + + let result = await roomProxy.redact(pollStartID) + + switch result { + case .success: + break + case .failure: + self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) + } + } + } + private func presentRoomMemberDetails(member: RoomMemberProxyProtocol) { guard let roomProxy else { fatalError() @@ -803,7 +850,7 @@ private extension RoomFlowCoordinator { case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) case messageForwarding(roomID: String, itemID: TimelineItemIdentifier) case notificationSettingsScreen(roomID: String) - case createPollForm(roomID: String) + case pollForm(roomID: String) } struct EventUserInfo { @@ -842,8 +889,8 @@ private extension RoomFlowCoordinator { case presentNotificationSettingsScreen case dismissNotificationSettingsScreen - case presentCreatePollForm - case dismissCreatePollForm + case presentPollForm(mode: PollFormMode) + case dismissPollForm } } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index fdbcd674c..366e9d392 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -884,9 +884,9 @@ public enum L10n { public static func screenCreatePollAnswerHint(_ p1: Int) -> String { return L10n.tr("Localizable", "screen_create_poll_answer_hint", p1) } - /// Are you sure you want to discard this poll? + /// Your changes wonโ€™t be saved public static var screenCreatePollDiscardConfirmation: String { return L10n.tr("Localizable", "screen_create_poll_discard_confirmation") } - /// Discard Poll + /// Cancel Poll public static var screenCreatePollDiscardConfirmationTitle: String { return L10n.tr("Localizable", "screen_create_poll_discard_confirmation_title") } /// Question or topic public static var screenCreatePollQuestionDesc: String { return L10n.tr("Localizable", "screen_create_poll_question_desc") } @@ -928,6 +928,10 @@ public enum L10n { public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") } /// Unblock user public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_unblock_user") } + /// Are you sure you want to delete this poll? + public static var screenEditPollDeleteConfirmation: String { return L10n.tr("Localizable", "screen_edit_poll_delete_confirmation") } + /// Delete Poll + public static var screenEditPollDeleteConfirmationTitle: String { return L10n.tr("Localizable", "screen_edit_poll_delete_confirmation_title") } /// Edit poll public static var screenEditPollTitle: String { return L10n.tr("Localizable", "screen_edit_poll_title") } /// Display name diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 5f334b4e1..02f48caa5 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2634,6 +2634,27 @@ class RoomProxyMock: RoomProxyProtocol { return createPollQuestionAnswersPollKindReturnValue } } + //MARK: - editPoll + + var editPollOriginalQuestionAnswersPollKindCallsCount = 0 + var editPollOriginalQuestionAnswersPollKindCalled: Bool { + return editPollOriginalQuestionAnswersPollKindCallsCount > 0 + } + var editPollOriginalQuestionAnswersPollKindReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? + var editPollOriginalQuestionAnswersPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] = [] + var editPollOriginalQuestionAnswersPollKindReturnValue: Result! + var editPollOriginalQuestionAnswersPollKindClosure: ((String, String, [String], Poll.Kind) async -> Result)? + + func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { + editPollOriginalQuestionAnswersPollKindCallsCount += 1 + editPollOriginalQuestionAnswersPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, pollKind: pollKind) + editPollOriginalQuestionAnswersPollKindReceivedInvocations.append((eventID: eventID, question: question, answers: answers, pollKind: pollKind)) + if let editPollOriginalQuestionAnswersPollKindClosure = editPollOriginalQuestionAnswersPollKindClosure { + return await editPollOriginalQuestionAnswersPollKindClosure(eventID, question, answers, pollKind) + } else { + return editPollOriginalQuestionAnswersPollKindReturnValue + } + } //MARK: - sendPollResponse var sendPollResponsePollStartIDAnswersCallsCount = 0 diff --git a/ElementX/Sources/Mocks/PollMock.swift b/ElementX/Sources/Mocks/PollMock.swift index e194f218f..2c0f8bc36 100644 --- a/ElementX/Sources/Mocks/PollMock.swift +++ b/ElementX/Sources/Mocks/PollMock.swift @@ -67,6 +67,15 @@ extension Poll { .mock(text: "USA ๐Ÿ‡บ๐Ÿ‡ธ", votes: 2, allVotes: 10)], ended: true) } + + static var emptyDisclosed: Self { + mock(question: "What country do you like most?", + pollKind: .disclosed, + options: [.mock(text: "Italy ๐Ÿ‡ฎ๐Ÿ‡น", votes: 0, allVotes: 0), + .mock(text: "China ๐Ÿ‡จ๐Ÿ‡ณ", votes: 0, allVotes: 0), + .mock(text: "USA ๐Ÿ‡บ๐Ÿ‡ธ", votes: 0, allVotes: 0)], + createdByAccountOwner: true) + } } extension Poll.Option { @@ -81,13 +90,13 @@ extension Poll.Option { } extension PollRoomTimelineItem { - static func mock(poll: Poll, isOutgoing: Bool = true) -> Self { + static func mock(poll: Poll, isOutgoing: Bool = true, isEditable: Bool = false) -> Self { .init(id: .init(timelineID: UUID().uuidString, eventID: UUID().uuidString), poll: poll, body: "poll", timestamp: "Now", isOutgoing: isOutgoing, - isEditable: false, + isEditable: isEditable, canBeRepliedTo: true, sender: .init(id: "userID"), properties: .init()) diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 9623d8ef0..ce485fd95 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -45,7 +45,7 @@ enum A11yIdentifiers { static let migrationScreen = MigrationScreen() static let notificationSettingsScreen = NotificationSettingsScreen() static let notificationSettingsEditScreen = NotificationSettingsEditScreen() - static let createPollScreen = CreatePollScreen() + static let pollFormScreen = PollFormScreen() struct AlertInfo { let primaryButton = "alert_info-primary_button" @@ -234,14 +234,13 @@ enum A11yIdentifiers { let roomTopic = "create_room-room_topic" } - struct CreatePollScreen { - let question = "create_poll-question" - let create = "create_poll-create" - let addOption = "create_poll-add_option" - let pollKind = "create_poll-kind" - - private let optionPrefix = "create_poll-option" + struct PollFormScreen { + let addOption = "poll_form-add_option" + let pollKind = "poll_form-kind" + let question = "poll_form-question" + let submit = "poll_form-submit" + private let optionPrefix = "poll_form-option" func optionID(_ index: Int) -> String { "\(optionPrefix)-\(index)" } diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift index dc47f48a2..41468e945 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift @@ -36,7 +36,7 @@ enum ComposerToolbarViewModelAction { case displayMediaPicker case displayDocumentPicker case displayLocationPicker - case displayPollForm + case displayNewPollForm case handlePasteOrDrop(provider: NSItemProvider) @@ -55,7 +55,7 @@ enum ComposerToolbarViewAction { case displayMediaPicker case displayDocumentPicker case displayLocationPicker - case displayPollForm + case displayNewPollForm case handlePasteOrDrop(provider: NSItemProvider) case enableTextFormatting case composerAction(action: ComposerAction) diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift index 778ccf2cb..1db108baf 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -128,8 +128,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool actionsSubject.send(.displayDocumentPicker) case .displayLocationPicker: actionsSubject.send(.displayLocationPicker) - case .displayPollForm: - actionsSubject.send(.displayPollForm) + case .displayNewPollForm: + actionsSubject.send(.displayNewPollForm) case .handlePasteOrDrop(let provider): actionsSubject.send(.handlePasteOrDrop(provider: provider)) case .enableTextFormatting: diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift index cff5270b8..5a29cf040 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -77,7 +77,7 @@ struct RoomAttachmentPicker: View { Button { context.showAttachmentPopover = false - context.send(viewAction: .displayPollForm) + context.send(viewAction: .displayNewPollForm) } label: { Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls) .labelStyle(.menuSheet) diff --git a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenModels.swift b/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenModels.swift deleted file mode 100644 index 697e1cafe..000000000 --- a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenModels.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// 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 CreatePollScreenViewModelAction { - case create(question: String, options: [String], pollKind: Poll.Kind) - case cancel -} - -struct CreatePollScreenViewState: BindableState { - let maxNumberOfOptions = 20 - var bindings: CreatePollScreenViewStateBindings = .init() -} - -struct CreatePollScreenViewStateBindings { - var question = "" - var options: [Option] = [.init(), .init()] - var isUndisclosed = false - - struct Option: Identifiable, Equatable { - let id = UUID() - var text = "" - } - - var isCreateButtonDisabled: Bool { - question.isEmpty || options.count < 2 || options.contains { $0.text.isEmpty } - } - - var hasContent: Bool { - !question.isEmpty || options.contains(where: { !$0.text.isEmpty }) || isUndisclosed - } - - var alertInfo: AlertInfo? -} - -enum CreatePollScreenViewAction { - case cancel - case create - case deleteOption(index: Int) - case addOption -} diff --git a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift similarity index 58% rename from ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift rename to ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift index 7f2a853ab..7e52aa154 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift @@ -17,27 +17,30 @@ import Combine import SwiftUI -struct CreatePollScreenCoordinatorParameters { } - -enum CreatePollScreenCoordinatorAction { - case cancel - case create(question: String, options: [String], pollKind: Poll.Kind) +struct PollFormScreenCoordinatorParameters { + let mode: PollFormMode } -final class CreatePollScreenCoordinator: CoordinatorProtocol { - private let parameters: CreatePollScreenCoordinatorParameters - private var viewModel: CreatePollScreenViewModelProtocol - private let actionsSubject: PassthroughSubject = .init() +enum PollFormScreenCoordinatorAction { + case cancel + case delete + case submit(question: String, options: [String], pollKind: Poll.Kind) +} + +final class PollFormScreenCoordinator: CoordinatorProtocol { + private let parameters: PollFormScreenCoordinatorParameters + private var viewModel: PollFormScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() - var actions: AnyPublisher { + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(parameters: CreatePollScreenCoordinatorParameters) { + init(parameters: PollFormScreenCoordinatorParameters) { self.parameters = parameters - viewModel = CreatePollScreenViewModel() + viewModel = PollFormScreenViewModel(mode: parameters.mode) } func start() { @@ -46,16 +49,18 @@ final class CreatePollScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case let .create(question, options, pollKind): - self.actionsSubject.send(.create(question: question, options: options, pollKind: pollKind)) case .cancel: self.actionsSubject.send(.cancel) + case .delete: + self.actionsSubject.send(.delete) + case let .submit(question, options, pollKind): + self.actionsSubject.send(.submit(question: question, options: options, pollKind: pollKind)) } } .store(in: &cancellables) } func toPresentable() -> AnyView { - AnyView(CreatePollScreen(context: viewModel.context)) + AnyView(PollFormScreen(context: viewModel.context)) } } diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift new file mode 100644 index 000000000..790131095 --- /dev/null +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift @@ -0,0 +1,122 @@ +// +// 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 PollFormScreenViewModelAction: Equatable { + case cancel + case delete + case submit(question: String, options: [String], pollKind: Poll.Kind) +} + +struct PollFormScreenViewState: BindableState { + let mode: PollFormMode + let maxNumberOfOptions = 20 + var bindings: PollFormScreenViewStateBindings = .init() + + init(mode: PollFormMode) { + self.mode = mode + + switch mode { + case .new: + bindings = .init() + case .edit(_, let poll): + bindings = .init(poll: poll) + } + } + + var navigationTitle: String { + switch mode { + case .new: + return L10n.screenCreatePollTitle + case .edit: + return L10n.screenEditPollTitle + } + } + + var submitButtonTitle: String { + switch mode { + case .new: + return L10n.actionCreate + case .edit: + return L10n.actionDone + } + } + + var isSubmitButtonDisabled: Bool { + switch mode { + case .new: + return !bindings.hasValidContent + case .edit: + return !bindings.hasValidContent || !formContentHasChanged + } + } + + var formContentHasChanged: Bool { + let initialBindings: PollFormScreenViewStateBindings + + switch mode { + case .new: + initialBindings = .init() + case .edit(_, let poll): + initialBindings = .init(poll: poll) + } + + return bindings != initialBindings + } +} + +enum PollFormMode: Hashable { + case new + case edit(eventID: String, poll: Poll) +} + +struct PollFormScreenViewStateBindings: Equatable { + var question = "" + var options: [Option] = [.init(), .init()] + var isUndisclosed = false + + struct Option: Identifiable, Equatable { + let id = UUID() + var text = "" + } + + var hasValidContent: Bool { + !question.isEmpty && options.count >= 2 && options.allSatisfy { !$0.text.isEmpty } + } + + var alertInfo: AlertInfo? + + static func == (lhs: PollFormScreenViewStateBindings, rhs: PollFormScreenViewStateBindings) -> Bool { + lhs.question == rhs.question && lhs.options.map(\.text) == rhs.options.map(\.text) && lhs.isUndisclosed == rhs.isUndisclosed + } +} + +extension PollFormScreenViewStateBindings { + init(poll: Poll) { + self.init(question: poll.question, + options: poll.options.map { .init(text: $0.text) }, + isUndisclosed: poll.kind == .undisclosed) + } +} + +enum PollFormScreenViewAction { + case cancel + case submit + case delete + case deleteOption(index: Int) + case addOption +} diff --git a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModel.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift similarity index 65% rename from ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModel.swift rename to ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift index 9ebe5fa9a..d4919cea7 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift @@ -17,29 +17,35 @@ import Combine import SwiftUI -typealias CreatePollScreenViewModelType = StateStoreViewModel +typealias PollFormScreenViewModelType = StateStoreViewModel -class CreatePollScreenViewModel: CreatePollScreenViewModelType, CreatePollScreenViewModelProtocol { - private var actionsSubject: PassthroughSubject = .init() +class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewModelProtocol { + private var actionsSubject: PassthroughSubject = .init() - var actions: AnyPublisher { + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - - init() { - super.init(initialViewState: .init()) + + init(mode: PollFormMode) { + super.init(initialViewState: .init(mode: mode)) } // MARK: - Public - override func process(viewAction: CreatePollScreenViewAction) { + override func process(viewAction: PollFormScreenViewAction) { switch viewAction { - case .create: - actionsSubject.send(.create(question: state.bindings.question, + case .submit: + actionsSubject.send(.submit(question: state.bindings.question, options: state.bindings.options.map(\.text), pollKind: state.bindings.isUndisclosed ? .undisclosed : .disclosed)) + case .delete: + state.bindings.alertInfo = .init(id: .init(), + title: L10n.screenEditPollDeleteConfirmationTitle, + message: L10n.screenEditPollDeleteConfirmation, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.actionOk, action: { self.actionsSubject.send(.delete) })) case .cancel: - if state.bindings.hasContent { + if state.formContentHasChanged { state.bindings.alertInfo = .init(id: .init(), title: L10n.screenCreatePollDiscardConfirmationTitle, message: L10n.screenCreatePollDiscardConfirmation, diff --git a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModelProtocol.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModelProtocol.swift similarity index 77% rename from ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModelProtocol.swift rename to ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModelProtocol.swift index 2f39a4ecf..2935156c7 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModelProtocol.swift @@ -17,7 +17,7 @@ import Combine @MainActor -protocol CreatePollScreenViewModelProtocol { - var actions: AnyPublisher { get } - var context: CreatePollScreenViewModelType.Context { get } +protocol PollFormScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: PollFormScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/CreatePollScreen/View/CreatePollScreen.swift b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift similarity index 69% rename from ElementX/Sources/Screens/CreatePollScreen/View/CreatePollScreen.swift rename to ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift index 6f3ec56c9..e076f0df3 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/View/CreatePollScreen.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift @@ -17,35 +17,36 @@ import Compound import SwiftUI -struct CreatePollScreen: View { - @ObservedObject var context: CreatePollScreenViewModel.Context +struct PollFormScreen: View { + @ObservedObject var context: PollFormScreenViewModel.Context @FocusState var focus: Focus? - + enum Focus: Hashable { case question case option(index: Int) } - + var body: some View { Form { questionSection optionsSection showResultsSection + deletePollSection } - .track(screen: .createPoll) + .trackAnalyticsIfNeeded(context: context) .compoundForm() .scrollDismissesKeyboard(.immediately) .environment(\.editMode, .constant(.active)) - .navigationTitle(L10n.screenCreatePollTitle) + .navigationTitle(context.viewState.navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .animation(.elementDefault, value: context.options) - .interactiveDismissDisabled(context.viewState.bindings.hasContent) + .interactiveDismissDisabled(context.viewState.formContentHasChanged) .alert(item: $context.alertInfo) } - + // MARK: - Private - + private var questionSection: some View { Section(L10n.screenCreatePollQuestionDesc) { TextField(text: $context.question) { @@ -57,7 +58,7 @@ struct CreatePollScreen: View { } .textFieldStyle(.compoundForm) .focused($focus, equals: .question) - .accessibilityIdentifier(A11yIdentifiers.createPollScreen.question) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.question) .onSubmit { focus = context.options.indices.first.map { .option(index: $0) } } @@ -65,22 +66,22 @@ struct CreatePollScreen: View { } .compoundFormSection() } - + private var optionsSection: some View { Section { ForEach(context.options) { option in if let index = context.options.firstIndex(of: option) { - CreatePollOptionView(text: $context.options[index].text.limited(to: 240), - placeholder: L10n.screenCreatePollAnswerHint(index + 1), - canDeleteItem: context.options.count > 2) { + PollFormOptionView(text: $context.options[index].text.limited(to: 240), + placeholder: L10n.screenCreatePollAnswerHint(index + 1), + canDeleteItem: context.options.count > 2) { if case .option(let focusedIndex) = focus, focusedIndex == index { focus = nil } - + context.send(viewAction: .deleteOption(index: index)) } .focused($focus, equals: .option(index: index)) - .accessibilityIdentifier(A11yIdentifiers.createPollScreen.optionID(index)) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.optionID(index)) .onSubmit { let nextOptionIndex = index == context.options.endIndex - 1 ? nil : index + 1 focus = nextOptionIndex.map { .option(index: $0) } @@ -91,26 +92,43 @@ struct CreatePollScreen: View { .onMove { offsets, toOffset in context.options.move(fromOffsets: offsets, toOffset: toOffset) } - + if context.options.count < context.viewState.maxNumberOfOptions { Button(L10n.screenCreatePollAddOptionBtn) { context.send(viewAction: .addOption) focus = context.options.indices.last.map { .option(index: $0) } } - .accessibilityIdentifier(A11yIdentifiers.createPollScreen.addOption) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.addOption) } } .compoundFormSection() } - + private var showResultsSection: some View { Section { Toggle(L10n.screenCreatePollAnonymousDesc, isOn: $context.isUndisclosed) - .accessibilityIdentifier(A11yIdentifiers.createPollScreen.pollKind) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.pollKind) } .compoundFormSection() } - + + @ViewBuilder + private var deletePollSection: some View { + switch context.viewState.mode { + case .edit: + Section { + Button(role: .destructive) { + context.send(viewAction: .delete) + } label: { + Text(L10n.actionDeletePoll) + } + } + .compoundFormSection() + case .new: + EmptyView() + } + } + @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { @@ -118,24 +136,36 @@ struct CreatePollScreen: View { context.send(viewAction: .cancel) } } - + ToolbarItem(placement: .confirmationAction) { - Button(L10n.actionCreate) { - context.send(viewAction: .create) + Button(context.viewState.submitButtonTitle) { + context.send(viewAction: .submit) } - .disabled(context.viewState.bindings.isCreateButtonDisabled) - .accessibilityIdentifier(A11yIdentifiers.createPollScreen.create) + .disabled(context.viewState.isSubmitButtonDisabled) + .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.submit) } } } -private struct CreatePollOptionView: View { +private extension View { + @MainActor @ViewBuilder + func trackAnalyticsIfNeeded(context: PollFormScreenViewModel.Context) -> some View { + switch context.viewState.mode { + case .edit: + self + case .new: + track(screen: .createPoll) + } + } +} + +private struct PollFormOptionView: View { @Environment(\.editMode) var editMode @Binding var text: String let placeholder: String let canDeleteItem: Bool let deleteAction: () -> Void - + var body: some View { HStack(spacing: 8) { if editMode?.wrappedValue == .active { @@ -160,11 +190,11 @@ private struct CreatePollOptionView: View { // MARK: - Previews -struct CreatePollScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = CreatePollScreenViewModel() +struct PollFormScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = PollFormScreenViewModel(mode: .new) static var previews: some View { NavigationStack { - CreatePollScreen(context: viewModel.context) + PollFormScreen(context: viewModel.context) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 9ae194fc4..4d53a2ca2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -36,7 +36,7 @@ enum RoomScreenCoordinatorAction { case presentMediaUploadPreviewScreen(URL) case presentRoomDetails case presentLocationPicker - case presentPollForm + case presentPollForm(mode: PollFormMode) case presentLocationViewer(body: String, geoURI: GeoURI, description: String?) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case presentRoomMemberDetails(member: RoomMemberProxyProtocol) @@ -104,8 +104,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentMediaUploadPicker(.documents)) case .displayLocationPicker: actionsSubject.send(.presentLocationPicker) - case .displayPollForm: - actionsSubject.send(.presentPollForm) + case .displayPollForm(let mode): + actionsSubject.send(.presentPollForm(mode: mode)) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.presentMediaUploadPreviewScreen(url)) case .displayRoomMemberDetails(let member): diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index 36fdb9c23..280ffa9f7 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -24,6 +24,7 @@ enum RoomScreenInteractionHandlerAction { case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) case displayMessageForwarding(itemID: TimelineItemIdentifier) case displayMediaUploadPreviewScreen(url: URL) + case displayPollForm(mode: PollFormMode) case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case showActionMenu(TimelineItemActionMenuInfo) case showDebugInfo(TimelineItemDebugInfo) @@ -185,30 +186,18 @@ class RoomScreenInteractionHandler { UIPasteboard.general.string = messageTimelineItem.body case .edit: - guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { - return - } - - let text: String - switch messageTimelineItem.contentType { - case .text(let textItem): - if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = textItem.formattedBodyHTMLString { - text = formattedBodyHTMLString - } else { - text = messageTimelineItem.body - } - case .emote(let emoteItem): - if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = emoteItem.formattedBodyHTMLString { - text = "/me " + formattedBodyHTMLString - } else { - text = "/me " + messageTimelineItem.body + switch timelineItem { + case let messageTimelineItem as EventBasedMessageTimelineItemProtocol: + processEditMessageEvent(messageTimelineItem) + case let pollTimelineItem as PollRoomTimelineItem: + guard let eventID = pollTimelineItem.id.eventID else { + MXLog.error("Cannot edit poll with id: \(timelineItem.id)") + return } + actionsSubject.send(.displayPollForm(mode: .edit(eventID: eventID, poll: pollTimelineItem.poll))) default: - text = messageTimelineItem.body + MXLog.error("Cannot edit item with id: \(timelineItem.id)") } - - actionsSubject.send(.composer(action: .setText(text: text))) - actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) case .copyPermalink: do { guard let eventID = eventTimelineItem.id.eventID else { @@ -258,6 +247,29 @@ class RoomScreenInteractionHandler { } } + private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) { + let text: String + switch messageTimelineItem.contentType { + case .text(let textItem): + if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = textItem.formattedBodyHTMLString { + text = formattedBodyHTMLString + } else { + text = messageTimelineItem.body + } + case .emote(let emoteItem): + if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = emoteItem.formattedBodyHTMLString { + text = "/me " + formattedBodyHTMLString + } else { + text = "/me " + messageTimelineItem.body + } + default: + text = messageTimelineItem.body + } + + actionsSubject.send(.composer(action: .setText(text: text))) + actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) + } + // MARK: Polls func sendPollResponse(pollStartID: String, optionID: String) { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 1ef38a3d1..3968223ee 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -28,7 +28,7 @@ enum RoomScreenViewModelAction { case displayMediaPicker case displayDocumentPicker case displayLocationPicker - case displayPollForm + case displayPollForm(mode: PollFormMode) case displayMediaUploadPreviewScreen(url: URL) case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case displayMessageForwarding(itemID: TimelineItemIdentifier) @@ -66,6 +66,7 @@ enum RoomScreenComposerMode: Equatable { enum RoomScreenViewPollAction { case selectOption(pollStartID: String, optionID: String) case end(pollStartID: String) + case edit(pollStartID: String, poll: Poll) } enum RoomScreenViewAudioAction { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 210990f48..3118b9531 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -188,8 +188,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayDocumentPicker) case .displayLocationPicker: actionsSubject.send(.displayLocationPicker) - case .displayPollForm: - actionsSubject.send(.displayPollForm) + case .displayNewPollForm: + actionsSubject.send(.displayPollForm(mode: .new)) case .handlePasteOrDrop(let provider): roomScreenInteractionHandler.handlePasteOrDrop(provider) case .composerModeChanged(mode: let mode): @@ -213,6 +213,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol message: L10n.commonPollEndConfirmation, primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), secondaryButton: .init(title: L10n.actionOk, action: { self.roomScreenInteractionHandler.endPoll(pollStartID: pollStartID) })) + case .edit(let pollStartID, let poll): + actionsSubject.send(.displayPollForm(mode: .edit(eventID: pollStartID, poll: poll))) } } @@ -319,6 +321,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) case .displayMessageForwarding(let itemID): actionsSubject.send(.displayMessageForwarding(itemID: itemID)) + case .displayPollForm(let mode): + actionsSubject.send(.displayPollForm(mode: mode)) case .displayReportContent(let itemID, let senderID): actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID)) case .displayMediaUploadPreviewScreen(let url): diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index aabd660b4..de17d52da 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -202,12 +202,8 @@ struct TimelineItemBubbledStylerView: View { @ViewBuilder var localizedSendInfo: some View { HStack(spacing: 4) { - if let timelineItem = timelineItem as? TextBasedRoomTimelineItem { - Text(timelineItem.localizedSendInfo) - } else { - Text(timelineItem.timestamp) - } - + Text(timelineItem.localizedSendInfo) + if adjustedDeliveryStatus == .sendingFailed { CompoundIcon(\.error, size: .xSmall, relativeTo: .compound.bodyXS) .accessibilityLabel(L10n.commonSendingFailed) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift index b965e62b5..bad5d887c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift @@ -89,11 +89,11 @@ struct PollRoomTimelineView: View { @ViewBuilder private var toolbarView: some View { - if !poll.hasEnded, poll.createdByAccountOwner, let eventID { + if !poll.hasEnded, poll.createdByAccountOwner { Button { - context.send(viewAction: .poll(.end(pollStartID: eventID))) + toolbarAction() } label: { - Text(L10n.actionEndPoll) + Text(timelineItem.isEditable ? L10n.actionEditPoll : L10n.actionEndPoll) .lineLimit(2, reservesSpace: false) .font(.compound.bodyLGSemibold) .foregroundColor(.compound.textOnSolidPrimary) @@ -108,6 +108,18 @@ struct PollRoomTimelineView: View { .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))) + } + } private func progressBarColor(for option: Poll.Option) -> Color { if poll.hasEnded { @@ -194,5 +206,15 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview { .environment(\.timelineStyle, .plain) .environmentObject(viewModel.context) .previewDisplayName("Creator, disclosed, Plain") + + PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true)) + .environment(\.timelineStyle, .bubbles) + .environmentObject(viewModel.context) + .previewDisplayName("Creator, no votes, Bubble") + + PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true)) + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) + .previewDisplayName("Creator, no votes, Plain") } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 156b80c70..e4f2aa717 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -726,6 +726,21 @@ class RoomProxy: RoomProxyProtocol { } } } + + func editPoll(original eventID: String, + question: String, + answers: [String], + pollKind: Poll.Kind) async -> Result { + await Task.dispatch(on: .global()) { + do { + let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID) + return try .success(self.room.editPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind), editItem: originalEvent)) + } catch { + MXLog.error("Failed editing the poll: \(error), eventID: \(eventID)") + return .failure(.failedEditingPoll) + } + } + } func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { await Task.dispatch(on: .global()) { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index d6b53fcd3..1de7dcec1 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -45,6 +45,7 @@ enum RoomProxyError: Error, Equatable { case failedCreatingPoll case failedSendingPollResponse case failedEndingPoll + case failedEditingPoll } // sourcery: AutoMockable @@ -186,6 +187,8 @@ protocol RoomProxyProtocol { func canUserTriggerRoomNotification(userID: String) async -> Result func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result + + func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result func sendPollResponse(pollStartID: String, answers: [String]) async -> Result diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index 88735c2f8..7aa20eed8 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -65,7 +65,7 @@ struct RoomEventStringBuilder { member: sender.id, memberIsYou: isOutgoing) .map(AttributedString.init) - case .poll(let question, _, _, _, _, _): + case .poll(let question, _, _, _, _, _, _): return prefix(L10n.commonPollSummary(question), with: senderDisplayName) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift index 41e9ee956..19d085cfc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift @@ -28,7 +28,7 @@ struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol { var properties: RoomTimelineItemProperties } -struct Poll: Equatable { +struct Poll: Hashable { let question: String let kind: Kind let maxSelections: Int @@ -41,13 +41,17 @@ struct Poll: Equatable { var hasEnded: Bool { endDate != nil } + + var hasVotes: Bool { + votes.values.contains(where: { !$0.isEmpty }) + } - enum Kind: Equatable { + enum Kind: Hashable { case disclosed case undisclosed } - struct Option: Equatable { + struct Option: Hashable { let id: String let text: String let votes: Int diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index b83f2f320..482f363a0 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -71,8 +71,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { avatarURLString: avatarUrl, previousAvatarURLString: prevAvatarUrl, isOutgoing: isOutgoing) - case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime): - return buildPollTimelineItem(question, kind, maxSelections, answers, votes, endTime, eventItemProxy, isOutgoing) + case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime, let edited): + return buildPollTimelineItem(question, kind, maxSelections, answers, votes, endTime, eventItemProxy, isOutgoing, edited) } } @@ -379,7 +379,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ votes: [String: [String]], _ endTime: UInt64?, _ eventItemProxy: EventTimelineItemProxy, - _ isOutgoing: Bool) -> RoomTimelineItemProtocol { + _ isOutgoing: Bool, + _ edited: Bool) -> RoomTimelineItemProtocol { let allVotes = votes.reduce(0) { count, pair in count + pair.value.count } @@ -410,10 +411,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { body: poll.question, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, - isEditable: eventItemProxy.isEditable, + // FIX ME: `eventItemProxy.isEditable` needs to be fixed on the rust side (now returns always false) + isEditable: eventItemProxy.isOwn && !poll.hasVotes && !poll.hasEnded, canBeRepliedTo: eventItemProxy.canBeRepliedTo, sender: eventItemProxy.sender, - properties: RoomTimelineItemProperties(isEdited: false, + properties: RoomTimelineItemProperties(isEdited: edited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) @@ -633,7 +635,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { switch timelineItem.kind() { case .message: return timelineItemReplyDetails(for: timelineItem.asMessage()?.msgtype(), sender: sender) - case .poll(let question, _, _, _, _, _): + case .poll(let question, _, _, _, _, _, _): replyContent = .poll(question: question) case .sticker(let body, _, _): replyContent = .message(.text(.init(body: body))) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 1dca56f56..93aae5a0a 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -876,7 +876,7 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .createPoll: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = CreatePollScreenCoordinator(parameters: .init()) + let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator } diff --git a/UITests/Sources/CreatePollScreenUITests.swift b/UITests/Sources/PollFormScreenUITests.swift similarity index 76% rename from UITests/Sources/CreatePollScreenUITests.swift rename to UITests/Sources/PollFormScreenUITests.swift index 4367c3f6e..a6bb04aa5 100644 --- a/UITests/Sources/CreatePollScreenUITests.swift +++ b/UITests/Sources/PollFormScreenUITests.swift @@ -17,7 +17,7 @@ import XCTest @MainActor -class CreatePollScreenUITests: XCTestCase { +class PollFormScreenUITests: XCTestCase { func testEmptyScreen() async throws { let app = Application.launch(.createPoll) try await app.assertScreenshot(.createPoll) @@ -25,19 +25,19 @@ class CreatePollScreenUITests: XCTestCase { func testFilledPoll() async throws { let app = Application.launch(.createPoll) - let questionTextField = app.textFields[A11yIdentifiers.createPollScreen.question] + let questionTextField = app.textFields[A11yIdentifiers.pollFormScreen.question] questionTextField.tap() questionTextField.typeText("Do you like polls?") - let option1TextField = app.textFields[A11yIdentifiers.createPollScreen.optionID(0)] + let option1TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(0)] option1TextField.tap() option1TextField.typeText("Yes") - let option2TextField = app.textFields[A11yIdentifiers.createPollScreen.optionID(1)] + let option2TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(1)] option2TextField.tap() option2TextField.typeText("No\n") - let createButton = app.buttons[A11yIdentifiers.createPollScreen.create] + let createButton = app.buttons[A11yIdentifiers.pollFormScreen.create] XCTAssertTrue(createButton.isEnabled) try await app.assertScreenshot(.createPoll, step: 1) @@ -45,8 +45,8 @@ class CreatePollScreenUITests: XCTestCase { func testMaxOptions() async throws { let app = Application.launch(.createPoll) - let createButton = app.buttons[A11yIdentifiers.createPollScreen.create] - let addOption = app.buttons[A11yIdentifiers.createPollScreen.addOption] + let createButton = app.buttons[A11yIdentifiers.pollFormScreen.create] + let addOption = app.buttons[A11yIdentifiers.pollFormScreen.addOption] for _ in 1...18 { if !addOption.exists { diff --git a/UnitTests/Sources/CreatePollScreenViewModelTests.swift b/UnitTests/Sources/CreatePollScreenViewModelTests.swift deleted file mode 100644 index 599896002..000000000 --- a/UnitTests/Sources/CreatePollScreenViewModelTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// 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 CreatePollScreenViewModelTests: XCTestCase { - var viewModel: CreatePollScreenViewModelProtocol! - - var context: CreatePollScreenViewModelType.Context { - viewModel.context - } - - override func setUpWithError() throws { - viewModel = CreatePollScreenViewModel() - } - - func testInitialState() { - XCTAssertEqual(context.options.count, 2) - XCTAssertTrue(context.options.allSatisfy(\.text.isEmpty)) - XCTAssertTrue(context.question.isEmpty) - XCTAssertTrue(context.viewState.bindings.isCreateButtonDisabled) - XCTAssertFalse(context.viewState.bindings.isUndisclosed) - } - - func testValidPoll() async throws { - context.question = "foo" - context.options[0].text = "bla1" - context.options[1].text = "bla2" - XCTAssertFalse(context.viewState.bindings.isCreateButtonDisabled) - - let deferred = deferFulfillment(viewModel.actions) { action in - switch action { - case .create: - return true - default: - return false - } - } - - context.send(viewAction: .create) - - let action = try await deferred.fulfill() - - guard case .create(let question, let options, let kind) = action else { - XCTFail("Unexpected action") - return - } - XCTAssertEqual(question, "foo") - XCTAssertEqual(options.count, 2) - XCTAssertEqual(options[0], "bla1") - XCTAssertEqual(options[1], "bla2") - XCTAssertEqual(kind, .disclosed) - } - - func testInvalidPollEmptyOption() { - context.question = "foo" - context.options[0].text = "bla" - context.options[1].text = "bla" - context.send(viewAction: .addOption) - XCTAssertTrue(context.viewState.bindings.isCreateButtonDisabled) - } -} diff --git a/UnitTests/Sources/PollFormScreenViewModelTests.swift b/UnitTests/Sources/PollFormScreenViewModelTests.swift new file mode 100644 index 000000000..76eff0879 --- /dev/null +++ b/UnitTests/Sources/PollFormScreenViewModelTests.swift @@ -0,0 +1,158 @@ +// +// 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 PollFormScreenViewModelTests: XCTestCase { + var viewModel: PollFormScreenViewModelProtocol! + + var context: PollFormScreenViewModelType.Context { + viewModel.context + } + + override func setUpWithError() throws { + viewModel = PollFormScreenViewModel(mode: .new) + } + + func testNewPollInitialState() async throws { + XCTAssertEqual(context.options.count, 2) + XCTAssertTrue(context.options.allSatisfy(\.text.isEmpty)) + XCTAssertTrue(context.question.isEmpty) + XCTAssertTrue(context.viewState.isSubmitButtonDisabled) + XCTAssertFalse(context.viewState.bindings.isUndisclosed) + + // Cancellation should work without confirmation + let deferred = deferFulfillment(viewModel.actions, until: { _ in true }) + context.send(viewAction: .cancel) + let action = try await deferred.fulfill() + XCTAssertNil(context.alertInfo) + XCTAssertEqual(action, .cancel) + } + + func testEditPollInitialState() async throws { + setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) + XCTAssertEqual(context.options.count, 3) + XCTAssertTrue(context.options.allSatisfy { !$0.text.isEmpty }) + XCTAssertFalse(context.question.isEmpty) + XCTAssertTrue(context.viewState.isSubmitButtonDisabled) + XCTAssertFalse(context.viewState.bindings.isUndisclosed) + + // Cancellation should work without confirmation + let deferred = deferFulfillment(viewModel.actions, until: { _ in true }) + context.send(viewAction: .cancel) + let action = try await deferred.fulfill() + XCTAssertNil(context.alertInfo) + XCTAssertEqual(action, .cancel) + } + + func testNewPollInvalidEmptyOption() { + context.question = "foo" + context.options[0].text = "bla" + context.options[1].text = "bla" + context.send(viewAction: .addOption) + XCTAssertTrue(context.viewState.isSubmitButtonDisabled) + } + + func testEditPollInvalidEmptyOption() { + setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) + context.send(viewAction: .addOption) + XCTAssertTrue(context.viewState.isSubmitButtonDisabled) + + // Cancellation requires a confirmation + context.send(viewAction: .cancel) + XCTAssertNotNil(context.alertInfo) + } + + func testEditPollSubmitButtonState() { + setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) + XCTAssertTrue(context.viewState.isSubmitButtonDisabled) + context.options[0].text = "foo" + XCTAssertFalse(context.viewState.isSubmitButtonDisabled) + + // Cancellation requires a confirmation + context.send(viewAction: .cancel) + XCTAssertNotNil(context.alertInfo) + } + + func testNewPollSubmit() async throws { + context.question = "foo" + context.options[0].text = "bla1" + context.options[1].text = "bla2" + XCTAssertFalse(context.viewState.isSubmitButtonDisabled) + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .submit: + return true + default: + return false + } + } + + context.send(viewAction: .submit) + + let action = try await deferred.fulfill() + + guard case .submit(let question, let options, let kind) = action else { + XCTFail("Unexpected action") + return + } + XCTAssertEqual(question, "foo") + XCTAssertEqual(options.count, 2) + XCTAssertEqual(options[0], "bla1") + XCTAssertEqual(options[1], "bla2") + XCTAssertEqual(kind, .disclosed) + } + + func testEditPollSubmit() async throws { + setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) + context.question = "What is your favorite country?" + context.options.append(.init(text: "France ๐Ÿ‡ซ๐Ÿ‡ท")) + XCTAssertFalse(context.viewState.isSubmitButtonDisabled) + + let deferred = deferFulfillment(viewModel.actions) { action in + switch action { + case .submit: + return true + default: + return false + } + } + + context.send(viewAction: .submit) + + let action = try await deferred.fulfill() + + guard case .submit(let question, let options, let kind) = action else { + XCTFail("Unexpected action") + return + } + XCTAssertEqual(question, "What is your favorite country?") + XCTAssertEqual(options.count, 4) + XCTAssertEqual(options[0], "Italy ๐Ÿ‡ฎ๐Ÿ‡น") + XCTAssertEqual(options[1], "China ๐Ÿ‡จ๐Ÿ‡ณ") + XCTAssertEqual(options[2], "USA ๐Ÿ‡บ๐Ÿ‡ธ") + XCTAssertEqual(options[3], "France ๐Ÿ‡ซ๐Ÿ‡ท") + XCTAssertEqual(kind, .disclosed) + } + + private func setupViewModel(mode: PollFormMode) { + viewModel = PollFormScreenViewModel(mode: mode) + } +} diff --git a/UnitTests/__Snapshots__/PreviewTests/test_createPollScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_pollFormScreen.1.png similarity index 100% rename from UnitTests/__Snapshots__/PreviewTests/test_createPollScreen.1.png rename to UnitTests/__Snapshots__/PreviewTests/test_pollFormScreen.1.png diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Bubble.png b/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Bubble.png new file mode 100644 index 000000000..5e582db5b --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Bubble.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d30f9c6f6c3cb995c24c099ce955c1607132267e50e532b6e305846c10f5529 +size 129170 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Plain.png b/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Plain.png new file mode 100644 index 000000000..04374f02c --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pollRoomTimelineView.Creator-no-votes-Plain.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b02c475e4e8069d557f9a690007ca08849768c99c3cd26ebaf3e7c3e909bf148 +size 121026 diff --git a/project.yml b/project.yml index d4d023a8f..3a169fb74 100644 --- a/project.yml +++ b/project.yml @@ -45,7 +45,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 0.0.4-november23 + exactVersion: 0.0.5-november23 # path: ../matrix-rust-sdk Compound: url: https://github.com/vector-im/compound-ios