Introduce a StartChatFlowCoordinator instead of handing a navigation stack to the Screen Coordinator. (#4674)

* Introduce a basic StartChatFlowCoordinator.

* Move the rest of the start chat flow from the screen coordinator into the flow coordinator.

* Add a UI test for the entire start chat flow.

* Refactor CreateRoom… to CreateRoomScreen…
This commit is contained in:
Doug
2025-10-30 16:12:56 +00:00
committed by GitHub
parent 401ff49882
commit 933891614c
68 changed files with 764 additions and 789 deletions
+44 -56
View File
@@ -78,7 +78,6 @@
0B05A35FF5D1ED9E8A0B41A7 /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */; };
0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; };
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; };
0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */; };
0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; };
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */; };
0C346A4AD174F441EDB1414E /* IdentityConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB76A9AFC6CCAD4998D9B045 /* IdentityConfirmationScreenViewModel.swift */; };
@@ -194,6 +193,7 @@
21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; };
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */; };
21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; };
221A7F92116722322AE150AE /* StartChatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0367E0AF1163F42292D788BD /* StartChatTests.swift */; };
22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
22B380C579C148BA0BFB5952 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
22BA593A5E19D8D3DE2FAA6F /* TimelineEventContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */; };
@@ -233,9 +233,7 @@
281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; };
2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7310D8DFE01AF45F0689C3AA /* Publisher.swift */; };
288408E6151D7BD3EBAA073A /* RoomScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */; };
28AB1614E749D1147A2AC6C2 /* CreateRoomScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2214C32EA81FA9168D923D4C /* CreateRoomScreenTests.swift */; };
28E8C44DD6E39BEB2A1599C8 /* AccessibilityTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CABD320DE5566D133890B24 /* AccessibilityTestsAppCoordinator.swift */; };
292827744227DF61C930BDDB /* CreateRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */; };
2932570AA418974979D16DED /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E6DE144D887A254F4CAF203 /* UserPreference.swift */; };
29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */; };
2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; };
@@ -399,7 +397,6 @@
47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; };
4764FC9A843D1F9865EDC29C /* EditRoomAddressScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */; };
478C363F63A5DFC3C83E334C /* MediaProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */; };
4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; };
4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */; };
48416BBEB8DDF3E4DED0EDB6 /* ElementCallServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */; };
484202C5D50983442D24D061 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; };
@@ -482,7 +479,6 @@
55DF6DEEF2CEEF40F84B53B0 /* VoiceMessageRoomPlaybackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3B41C36800DD4558D7BDA7 /* VoiceMessageRoomPlaybackView.swift */; };
5618ED25F092DF5712003829 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; };
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */; };
564BF06B3E93D6DD55F903B2 /* CreateRoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */; };
565868808A1DA565707394ED /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; };
56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */; };
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; };
@@ -883,6 +879,7 @@
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */; };
9DBF6524DFD8143A4D6A17F0 /* CreateRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */; };
9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; };
9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; };
@@ -896,6 +893,7 @@
9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; };
9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */; };
9FBF0078657CA4F2A9747E98 /* SpaceRoomListProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */; };
9FC79DA30AE0E1502DAEBD51 /* StartChatFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477A8C656B7B26E99C35927F /* StartChatFlowCoordinator.swift */; };
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; };
@@ -947,6 +945,7 @@
A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; };
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
A7DB75E090542331F6668A23 /* CreateRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */; };
A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */; };
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; };
@@ -1080,7 +1079,6 @@
C280BD841DA2EB6B5BC84F84 /* MapTilerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */; };
C2879369106A419A5071F1F8 /* VoiceMessageRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */; };
C292A8DA07E6DBD66E946383 /* RageshakeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */; };
C32765D740C81AD4C42E8F50 /* CreateRoomFlowParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */; };
C3317EF833AB4060988DF098 /* SAS.strings in Resources */ = {isa = PBXBuildFile; fileRef = 135FC689EA39AE1D34153B58 /* SAS.strings */; };
C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; };
C3AFDF6349E54290AA31EC88 /* preview_video.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 45A4B934BA41D6C255900265 /* preview_video.jpg */; };
@@ -1148,6 +1146,7 @@
CE3B7FC34FB2C279AAA5EA01 /* AVMetadataMachineReadableCodeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */; };
CE4B342F9DD747CF4BEDB5AB /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43F773904F87FF5ADFE4DD1 /* TestablePreview.swift */; };
CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */; };
CE8296D4AD30DDC6D0C67A74 /* CreateRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFD1CFB730C7417925264F17 /* CreateRoomScreen.swift */; };
CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; };
CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */; };
CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; };
@@ -1177,6 +1176,7 @@
D31B34B3902BC597593F3ABB /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; };
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; };
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; };
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; };
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; };
D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; };
@@ -1204,6 +1204,7 @@
DA10C99BA43A0F1E732F6274 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; };
DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; };
DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; };
DB01FDD050A43B72C5425549 /* CreateRoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC5E094DFE18C3AB7619D34 /* CreateRoomScreenViewModelProtocol.swift */; };
DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; };
DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */; };
DB65401349C143DFF883E2B0 /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EC6EA7EDFCE46710DA306 /* AnalyticsPromptScreenViewModel.swift */; };
@@ -1379,7 +1380,6 @@
F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */; };
F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */; };
F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DFF217B3D9D0941283278C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift */; };
F9788AE0B9EA19F1190ED14F /* StartChatScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B927A399A5418DA40A5CA15 /* StartChatScreenTests.swift */; };
F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; };
F996259EAE68664BC345C197 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DBE1C69F3A60D93B2203F /* Application.swift */; };
F99FB21EFC6D99D247FE7CBE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; };
@@ -1390,7 +1390,6 @@
FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; };
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; };
FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; };
FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; };
FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */; };
FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; };
@@ -1517,6 +1516,7 @@
03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
0367E0AF1163F42292D788BD /* StartChatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatTests.swift; sourceTree = "<group>"; };
0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = "<group>"; };
03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1556,6 +1556,7 @@
0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; };
0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0AC5E094DFE18C3AB7619D34 /* CreateRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; };
0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -1569,6 +1570,7 @@
0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = "<group>"; };
0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenModels.swift; sourceTree = "<group>"; };
0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenCoordinator.swift; sourceTree = "<group>"; };
0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = "<group>"; };
@@ -1615,7 +1617,6 @@
1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenCoordinator.swift; sourceTree = "<group>"; };
15748C254911E3654C93B0ED /* MentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionBuilder.swift; sourceTree = "<group>"; };
1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorProtocol.swift; sourceTree = "<group>"; };
15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = "<group>"; };
161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = "<group>"; };
1627F2D56477BD331F6D732C /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = "<group>"; };
166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1686,7 +1687,6 @@
218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = "<group>"; };
21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
21DD8599815136EFF5B73F38 /* UserFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlowTests.swift; sourceTree = "<group>"; };
2214C32EA81FA9168D923D4C /* CreateRoomScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenTests.swift; sourceTree = "<group>"; };
226A56746AD99ABDA97189E1 /* SpaceServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxy.swift; sourceTree = "<group>"; };
22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = "<group>"; };
227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
@@ -1786,7 +1786,6 @@
3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObject.swift; sourceTree = "<group>"; };
33AE897D86784CCA5E4E9227 /* ElementCallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallService.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelProtocol.swift; sourceTree = "<group>"; };
342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelProtocol.swift; sourceTree = "<group>"; };
345172AD4377E83A44BD864F /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = "<group>"; };
347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1817,7 +1816,6 @@
3AB34956C87731AB094DB33A /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = "<group>"; };
3AD253E7EFF88F308D644272 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SAS.strings"; sourceTree = "<group>"; };
3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = "<group>"; };
3B927A399A5418DA40A5CA15 /* StartChatScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenTests.swift; sourceTree = "<group>"; };
3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = "<group>"; };
3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = "<group>"; };
3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1894,6 +1892,7 @@
471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = "<group>"; };
475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = "<group>"; };
477A8C656B7B26E99C35927F /* StartChatFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatFlowCoordinator.swift; sourceTree = "<group>"; };
47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; };
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
@@ -2028,6 +2027,7 @@
646B50583A2CE6DA67F7739A /* SpaceScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreen.swift; sourceTree = "<group>"; };
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = "<group>"; };
6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = "<group>"; };
64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenCoordinator.swift; sourceTree = "<group>"; };
64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = "<group>"; };
6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelTests.swift; sourceTree = "<group>"; };
653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -2137,7 +2137,6 @@
7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunner.swift; sourceTree = "<group>"; };
7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenModels.swift; sourceTree = "<group>"; };
7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomModels.swift; sourceTree = "<group>"; };
7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCoordinatorProtocol.swift; sourceTree = "<group>"; };
7BBADF8010C813D905C172CE /* SecurityAndPrivacyScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityAndPrivacyScreenModels.swift; sourceTree = "<group>"; };
7BD5523BDEDB247E29228476 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
@@ -2272,7 +2271,6 @@
933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaFrame.swift; sourceTree = "<group>"; };
9342F5D6729627B6393AF853 /* ServerConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenModels.swift; sourceTree = "<group>"; };
9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = "<group>"; };
935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomFlowParameters.swift; sourceTree = "<group>"; };
93C713D124FE915ABF47A6B7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = "<group>"; };
93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObjectExtensionsTest.swift; sourceTree = "<group>"; };
@@ -2547,7 +2545,6 @@
C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = "<group>"; };
C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = "<group>"; };
C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinator.swift; sourceTree = "<group>"; };
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
@@ -2592,6 +2589,7 @@
CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenViewModel.swift; sourceTree = "<group>"; };
CF847A34FC4C8C937CD39E08 /* LabsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = "<group>"; };
D01FD1171FF40E34D707FD00 /* BigIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigIcon.swift; sourceTree = "<group>"; };
@@ -2766,6 +2764,7 @@
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyMock.swift; sourceTree = "<group>"; };
EF98A02DED04075F7CF0C721 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
EFD1CFB730C7417925264F17 /* CreateRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreen.swift; sourceTree = "<group>"; };
EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = "<group>"; };
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = "<group>"; };
F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissionsTests.swift; sourceTree = "<group>"; };
@@ -2819,7 +2818,6 @@
FA723686F23EF45E2B398FBC /* TestablePreviewsDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreviewsDictionary.swift; sourceTree = "<group>"; };
FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
FABAC5C4373B0EC24D399663 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/SAS.strings"; sourceTree = "<group>"; };
FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreen.swift; sourceTree = "<group>"; };
FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModel.swift; sourceTree = "<group>"; };
FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformCursorView.swift; sourceTree = "<group>"; };
FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; };
@@ -3055,7 +3053,6 @@
0ED3F5C21537519389C07644 /* BugReport */,
8039515BAA53B7C3275AC64A /* Client */,
8B5E91450E85A9689931B221 /* ComposerDraft */,
8C3BAE06B336D97DABBE2509 /* CreateRoom */,
92E99C57D7F92ED16F73282C /* ElementCall */,
39557ADF21345E18F3865B9E /* Emojis */,
CA555F7C7CA382ACACF0D82B /* Keychain */,
@@ -4305,6 +4302,7 @@
5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */,
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */,
BDE3EDEA7E64D68FEB828F83 /* SpaceSettingsFlowCoordinator.swift */,
477A8C656B7B26E99C35927F /* StartChatFlowCoordinator.swift */,
C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */,
);
path = FlowCoordinators;
@@ -4962,6 +4960,18 @@
path = View;
sourceTree = "<group>";
};
821EB0D1C0019E3C7BBAEDBB /* CreateRoomScreen */ = {
isa = PBXGroup;
children = (
64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */,
0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */,
CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */,
0AC5E094DFE18C3AB7619D34 /* CreateRoomScreenViewModelProtocol.swift */,
A7694BCE812C4D7B2B1B42DF /* View */,
);
path = CreateRoomScreen;
sourceTree = "<group>";
};
823ED0EC3F1B6CF47D284011 /* Tools */ = {
isa = PBXGroup;
children = (
@@ -5117,14 +5127,6 @@
path = ComposerDraft;
sourceTree = "<group>";
};
8C3BAE06B336D97DABBE2509 /* CreateRoom */ = {
isa = PBXGroup;
children = (
935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */,
);
path = CreateRoom;
sourceTree = "<group>";
};
8F074E22FD93E64211971845 /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -5252,7 +5254,6 @@
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */,
37F46CC4FD89ECF4CF26391A /* AuthenticationFlowCoordinatorTests.swift */,
27DF257F5D968E5DD719583C /* BugReportTests.swift */,
2214C32EA81FA9168D923D4C /* CreateRoomScreenTests.swift */,
89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */,
57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */,
DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */,
@@ -5261,7 +5262,7 @@
D97A4E73EA97CA08D2BB9806 /* RoomScreenTests.swift */,
6640DB5B9171D163E6742639 /* ServerSelectionTests.swift */,
D8AA084E10B80D64449C02A9 /* SessionVerificationTests.swift */,
3B927A399A5418DA40A5CA15 /* StartChatScreenTests.swift */,
0367E0AF1163F42292D788BD /* StartChatTests.swift */,
F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */,
);
path = Sources;
@@ -5522,6 +5523,14 @@
path = View;
sourceTree = "<group>";
};
A7694BCE812C4D7B2B1B42DF /* View */ = {
isa = PBXGroup;
children = (
EFD1CFB730C7417925264F17 /* CreateRoomScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
A78C2592419CA4C76FBA8FD2 /* Application */ = {
isa = PBXGroup;
children = (
@@ -5839,18 +5848,6 @@
path = MapLibre;
sourceTree = "<group>";
};
C18958141C8ED6D778F779A4 /* CreateRoom */ = {
isa = PBXGroup;
children = (
C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */,
7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */,
15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */,
340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */,
F84100ED0C09031BAB7BB77E /* View */,
);
path = CreateRoom;
sourceTree = "<group>";
};
C1CD278862878F9545608040 /* SessionVerificationScreen */ = {
isa = PBXGroup;
children = (
@@ -6233,7 +6230,7 @@
53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */,
1185EECDD07495D65AC84AFC /* CallScreen */,
90DC2E28718955ED87AD1456 /* CreatePollScreen */,
C18958141C8ED6D778F779A4 /* CreateRoom */,
821EB0D1C0019E3C7BBAEDBB /* CreateRoomScreen */,
3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */,
45F2BCFD6E9A6F040CC20582 /* EditRoomAddressScreen */,
F5A65D1D3B83593598DC278D /* EmojiPickerScreen */,
@@ -6486,14 +6483,6 @@
path = IdentityConfirmedScreen;
sourceTree = "<group>";
};
F84100ED0C09031BAB7BB77E /* View */ = {
isa = PBXGroup;
children = (
FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
FA1D480A302295CFC3582543 /* View */ = {
isa = PBXGroup;
children = (
@@ -7718,12 +7707,11 @@
EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */,
AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */,
C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */,
564BF06B3E93D6DD55F903B2 /* CreateRoomCoordinator.swift in Sources */,
C32765D740C81AD4C42E8F50 /* CreateRoomFlowParameters.swift in Sources */,
FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */,
292827744227DF61C930BDDB /* CreateRoomScreen.swift in Sources */,
0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */,
4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */,
CE8296D4AD30DDC6D0C67A74 /* CreateRoomScreen.swift in Sources */,
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */,
9DBF6524DFD8143A4D6A17F0 /* CreateRoomScreenModels.swift in Sources */,
A7DB75E090542331F6668A23 /* CreateRoomScreenViewModel.swift in Sources */,
DB01FDD050A43B72C5425549 /* CreateRoomScreenViewModelProtocol.swift in Sources */,
565868808A1DA565707394ED /* CurrentValuePublisher.swift in Sources */,
A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */,
12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */,
@@ -8346,6 +8334,7 @@
9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */,
DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */,
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */,
9FC79DA30AE0E1502DAEBD51 /* StartChatFlowCoordinator.swift in Sources */,
3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */,
6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */,
C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */,
@@ -8541,7 +8530,6 @@
CB07184D37D5D65327A5A693 /* AuthenticationFlowCoordinatorTests.swift in Sources */,
9EF72884B74BC39B01640098 /* BugReportTests.swift in Sources */,
94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */,
28AB1614E749D1147A2AC6C2 /* CreateRoomScreenTests.swift in Sources */,
8D24671992A1C1753B211221 /* EncryptionResetTests.swift in Sources */,
E5E43A0CA99AF5BA11B194A2 /* EncryptionSettingsTests.swift in Sources */,
A950C95855C474F75B30CA7B /* PollFormScreenTests.swift in Sources */,
@@ -8550,7 +8538,7 @@
94EB81606839C175CA6C2ADB /* RoomScreenTests.swift in Sources */,
E9985DCD1B0D026D7E8BF809 /* ServerSelectionTests.swift in Sources */,
D5C2DA52162A978743A183F2 /* SessionVerificationTests.swift in Sources */,
F9788AE0B9EA19F1190ED14F /* StartChatScreenTests.swift in Sources */,
221A7F92116722322AE150AE /* StartChatTests.swift in Sources */,
54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */,
84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */,
B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */,
@@ -36,9 +36,10 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
// periphery:ignore - retaining purpose
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
// periphery:ignore - retaining purpose
private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator?
// periphery:ignore - retaining purpose
private var startChatFlowCoordinator: StartChatFlowCoordinator?
// periphery:ignore - retaining purpose
private var globalSearchScreenCoordinator: GlobalSearchScreenCoordinator?
@@ -223,12 +224,12 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case (.roomList, .startEncryptionResetFlow, .encryptionResetFlow):
startEncryptionResetFlow(animated: animated)
case (.encryptionResetFlow, .finishedEncryptionResetFlow, .roomList):
break
encryptionResetFlowCoordinator = nil
case (.roomList, .showStartChatScreen, .startChatScreen):
presentStartChat(animated: animated)
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
break
case (.roomList, .startStartChatFlow, .startChatFlow):
startStartChatFlow(animated: animated)
case (.startChatFlow, .finishedStartChatFlow, .roomList):
startChatFlowCoordinator = nil
case (.roomList, .showRoomDirectorySearchScreen, .roomDirectorySearchScreen):
presentRoomDirectorySearch()
@@ -398,7 +399,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case .presentEncryptionResetScreen:
stateMachine.processEvent(.startEncryptionResetFlow)
case .presentStartChatScreen:
stateMachine.processEvent(.showStartChatScreen)
stateMachine.processEvent(.startStartChatFlow)
case .presentGlobalSearch:
presentGlobalSearch()
case .logout:
@@ -565,39 +566,34 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Start Chat
private func presentStartChat(animated: Bool) {
let startChatNavigationStackCoordinator = NavigationStackCoordinator()
let userDiscoveryService = UserDiscoveryService(clientProxy: userSession.clientProxy)
let parameters = StartChatScreenCoordinatorParameters(orientationManager: flowParameters.windowManager,
userSession: userSession,
userIndicatorController: flowParameters.userIndicatorController,
navigationStackCoordinator: startChatNavigationStackCoordinator,
userDiscoveryService: userDiscoveryService,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings),
appSettings: flowParameters.appSettings,
analytics: flowParameters.analytics)
private func startStartChatFlow(animated: Bool) {
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = StartChatFlowCoordinator(userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
navigationStackCoordinator: navigationStackCoordinator,
flowParameters: flowParameters)
let coordinator = StartChatScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .close:
navigationSplitCoordinator.setSheetCoordinator(nil)
case .openRoom(let roomID):
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
case .openRoomDirectorySearch:
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.showRoomDirectorySearchScreen)
coordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .finished(let roomID):
navigationSplitCoordinator.setSheetCoordinator(nil)
if let roomID {
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
}
case .showRoomDirectory:
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.showRoomDirectorySearchScreen)
}
}
}
.store(in: &cancellables)
startChatNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationSplitCoordinator.setSheetCoordinator(startChatNavigationStackCoordinator, animated: animated) { [weak self] in
self?.stateMachine.processEvent(.dismissedStartChatScreen)
.store(in: &cancellables)
startChatFlowCoordinator = coordinator
coordinator.start()
navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator, animated: animated) { [weak self] in
self?.stateMachine.processEvent(.finishedStartChatFlow)
}
}
@@ -639,10 +635,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .resetComplete:
encryptionResetFlowCoordinator = nil
navigationSplitCoordinator.setSheetCoordinator(nil)
case .cancel:
encryptionResetFlowCoordinator = nil
navigationSplitCoordinator.setSheetCoordinator(nil)
}
}
@@ -33,8 +33,8 @@ class ChatsFlowCoordinatorStateMachine {
/// Showing the encryption reset flow.
case encryptionResetFlow(detailState: DetailState?)
/// Showing the start chat screen
case startChatScreen(detailState: DetailState?)
/// Showing the start chat flow
case startChatFlow(detailState: DetailState?)
/// Showing the logout flows
case logoutConfirmationScreen(detailState: DetailState?)
@@ -61,7 +61,7 @@ class ChatsFlowCoordinatorStateMachine {
.feedbackScreen(let detailState),
.recoveryKeyScreen(let detailState),
.encryptionResetFlow(let detailState),
.startChatScreen(let detailState),
.startChatFlow(let detailState),
.logoutConfirmationScreen(let detailState),
.roomDirectorySearchScreen(let detailState),
.reportRoomScreen(let detailState),
@@ -111,10 +111,10 @@ class ChatsFlowCoordinatorStateMachine {
/// The encryption reset flow is complete and has been dismissed.
case finishedEncryptionResetFlow
/// Request the start of the start chat flow
case showStartChatScreen
/// Start chat has been dismissed
case dismissedStartChatScreen
/// Request the start of the start chat flow.
case startStartChatFlow
/// The Start Chat flow is complete and has been dismissed.
case finishedStartChatFlow
/// Request presentation of the room directory search screen.
case showRoomDirectorySearchScreen
@@ -177,9 +177,9 @@ class ChatsFlowCoordinatorStateMachine {
case (.encryptionResetFlow(let detailState), .finishedEncryptionResetFlow):
return .roomList(detailState: detailState)
case (.roomList(let detailState), .showStartChatScreen):
return .startChatScreen(detailState: detailState)
case (.startChatScreen(let detailState), .dismissedStartChatScreen):
case (.roomList(let detailState), .startStartChatFlow):
return .startChatFlow(detailState: detailState)
case (.startChatFlow(let detailState), .finishedStartChatFlow):
return .roomList(detailState: detailState)
case (.roomList(let detailState), .showRoomDirectorySearchScreen):
@@ -1267,12 +1267,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentInviteUsersScreen() {
let selectedUsersSubject: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
let stackCoordinator = NavigationStackCoordinator()
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: userSession,
selectedUsers: .init(selectedUsersSubject),
roomType: .room(roomProxy: roomProxy),
roomProxy: roomProxy,
isSkippable: false,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
userIndicatorController: flowParameters.userIndicatorController,
appSettings: flowParameters.appSettings)
@@ -1286,8 +1284,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .proceed:
fatalError("Not handled in this flow.")
}
}
.store(in: &cancellables)
@@ -255,8 +255,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
private func presentInviteUsersScreen() {
let stackCoordinator = NavigationStackCoordinator()
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: flowParameters.userSession,
selectedUsers: nil,
roomType: .room(roomProxy: roomProxy),
roomProxy: roomProxy,
isSkippable: false,
userDiscoveryService: UserDiscoveryService(clientProxy: flowParameters.userSession.clientProxy),
userIndicatorController: flowParameters.userIndicatorController,
appSettings: flowParameters.appSettings)
@@ -270,8 +270,6 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .proceed:
fatalError("Not handled in this flow.")
}
}
.store(in: &cancellables)
@@ -0,0 +1,237 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import Foundation
import SwiftState
enum StartChatFlowCoordinatorAction {
case finished(roomID: String?)
case showRoomDirectory
}
class StartChatFlowCoordinator: FlowCoordinatorProtocol {
private let userDiscoveryService: UserDiscoveryServiceProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let flowParameters: CommonFlowParameters
private var createRoomScreenCoordinator: CreateRoomScreenCoordinator?
indirect enum State: StateType {
/// The state machine hasn't started.
case initial
/// Shown when the flow is started with options to create a room/DM, join by alias, use the room directory etc.
case startChat
/// The user is creating a new room.
case createRoom
/// The user is selecting an avatar for the new room.
case roomAvatarPicker
/// The user is inviting users to a newly created room.
case inviteUsers
}
enum Event: EventType {
/// The flow is being started.
case start
/// The user would like to create a room.
case createRoom
/// The user dismissed the create room screen.
case dismissedCreateRoom
/// The user would like to pick an avatar for the room.
case presentRoomAvatarPicker
/// The user finished picking the avatar.
case dismissedRoomAvatarPicker
/// The user's room was created successfully.
case createdRoom
}
private let stateMachine: StateMachine<State, Event>
private var cancellables: Set<AnyCancellable> = []
private let actionsSubject: PassthroughSubject<StartChatFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<StartChatFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(userDiscoveryService: UserDiscoveryServiceProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
flowParameters: CommonFlowParameters) {
self.userDiscoveryService = userDiscoveryService
self.navigationStackCoordinator = navigationStackCoordinator
self.flowParameters = flowParameters
stateMachine = .init(state: .initial)
configureStateMachine()
}
func start(animated: Bool) {
stateMachine.tryEvent(.start)
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
// There aren't any routes to this screen yet, so clear the stacks.
clearRoute(animated: animated)
}
func clearRoute(animated: Bool) {
switch stateMachine.state {
case .initial:
break
case .startChat:
navigationStackCoordinator.setRootCoordinator(nil, animated: animated) // StartChatScreen
case .createRoom:
navigationStackCoordinator.pop(animated: animated) // CreateRoomScreen
navigationStackCoordinator.setRootCoordinator(nil, animated: animated) // StartChatScreen
case .roomAvatarPicker:
navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // Media Picker
clearRoute(animated: animated) // Re-run with the state machine back in the .createRoom state.
case .inviteUsers:
navigationStackCoordinator.pop(animated: animated) // InviteUsersScreen
navigationStackCoordinator.pop(animated: animated) // CreateRoomScreen
navigationStackCoordinator.setRootCoordinator(nil, animated: animated) // StartChatScreen
}
}
// MARK: - Private
private func configureStateMachine() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .startChat]) { [weak self] _ in
self?.presentStartChatScreen()
}
stateMachine.addRoutes(event: .createRoom, transitions: [.startChat => .createRoom]) { [weak self] _ in
self?.presentCreateRoomScreen()
}
stateMachine.addRoutes(event: .dismissedCreateRoom, transitions: [.createRoom => .startChat]) { [weak self] _ in
self?.createRoomScreenCoordinator = nil
}
stateMachine.addRoutes(event: .presentRoomAvatarPicker, transitions: [.createRoom => .roomAvatarPicker]) { [weak self] context in
guard let mode = context.userInfo as? MediaPickerScreenMode else {
fatalError("A picker mode is required for the room avatar.")
}
self?.presentRoomAvatarPicker(mode)
}
stateMachine.addRoutes(event: .dismissedRoomAvatarPicker, transitions: [.roomAvatarPicker => .createRoom])
stateMachine.addRoutes(event: .createdRoom, transitions: [.createRoom => .inviteUsers]) { [weak self] context in
guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else {
fatalError("A room proxy is required to invite users.")
}
self?.presentInviteUsersScreen(roomProxy: roomProxy)
}
stateMachine.addErrorHandler { context in
if context.fromState == context.toState {
MXLog.error("Transition between equal states: \(context.fromState)")
} else {
fatalError("Unexpected transition: \(context)")
}
}
}
private func presentStartChatScreen() {
let parameters = StartChatScreenCoordinatorParameters(userSession: flowParameters.userSession,
userDiscoveryService: userDiscoveryService,
userIndicatorController: flowParameters.userIndicatorController,
appSettings: flowParameters.appSettings,
analytics: flowParameters.analytics)
let coordinator = StartChatScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .close:
actionsSubject.send(.finished(roomID: nil))
case .createRoom:
stateMachine.tryEvent(.createRoom)
case .openRoom(let roomID):
actionsSubject.send(.finished(roomID: roomID))
case .openRoomDirectorySearch:
actionsSubject.send(.showRoomDirectory)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(coordinator)
}
private func presentCreateRoomScreen() {
let createParameters = CreateRoomScreenCoordinatorParameters(userSession: flowParameters.userSession,
userIndicatorController: flowParameters.userIndicatorController,
appSettings: flowParameters.appSettings,
analytics: flowParameters.analytics)
let coordinator = CreateRoomScreenCoordinator(parameters: createParameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .createdRoom(let roomProxy):
stateMachine.tryEvent(.createdRoom, userInfo: roomProxy)
case .displayMediaPickerWithMode(let mode):
stateMachine.tryEvent(.presentRoomAvatarPicker, userInfo: mode)
}
}
.store(in: &cancellables)
createRoomScreenCoordinator = coordinator
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissedCreateRoom)
}
}
private func presentRoomAvatarPicker(_ mode: MediaPickerScreenMode) {
let stackCoordinator = NavigationStackCoordinator()
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
userIndicatorController: flowParameters.userIndicatorController,
orientationManager: flowParameters.windowManager) { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
case .selectedMediaAtURLs(let urls):
guard urls.count == 1 else { fatalError("Received an invalid number of URLs") }
navigationStackCoordinator.setSheetCoordinator(nil)
createRoomScreenCoordinator?.updateAvatar(fileURL: urls[0])
}
}
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissedRoomAvatarPicker)
}
}
private func presentInviteUsersScreen(roomProxy: JoinedRoomProxyProtocol) {
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: flowParameters.userSession,
roomProxy: roomProxy,
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: flowParameters.userIndicatorController,
appSettings: flowParameters.appSettings)
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
actionsSubject.send(.finished(roomID: roomProxy.id))
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
}
@@ -123,6 +123,7 @@ extension JoinedRoomProxyMock {
powerLevelsReturnValue = .success(powerLevelsProxyMock)
inviteUserIDReturnValue = .success(())
kickUserReasonReturnValue = .success(())
banUserReasonReturnValue = .success(())
unbanUserReturnValue = .success(())
@@ -270,8 +270,11 @@ enum A11yIdentifiers {
}
struct CreateRoomScreen {
let create = "create_room-create"
let roomAvatar = "create_room-room_avatar"
let roomName = "create_room-room_name"
let roomTopic = "create_room-room_topic"
let mediaPicker = "create_room-media_picker"
}
struct PollFormScreen {
@@ -1,71 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct CreateRoomCoordinatorParameters {
let userSession: UserSessionProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
let selectedUsers: [UserProfileProxy]
let appSettings: AppSettings
let analytics: AnalyticsService
}
enum CreateRoomCoordinatorAction {
case openRoom(withIdentifier: String)
case updateSelectedUsers([UserProfileProxy])
case updateDetails(CreateRoomFlowParameters)
case displayMediaPickerWithMode(MediaPickerScreenMode)
case removeImage
}
final class CreateRoomCoordinator: CoordinatorProtocol {
private var viewModel: CreateRoomViewModelProtocol
private let actionsSubject: PassthroughSubject<CreateRoomCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<CreateRoomCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: CreateRoomCoordinatorParameters) {
viewModel = CreateRoomViewModel(userSession: parameters.userSession,
createRoomParameters: parameters.createRoomParameters,
selectedUsers: parameters.selectedUsers,
analytics: parameters.analytics,
userIndicatorController: parameters.userIndicatorController,
appSettings: parameters.appSettings)
}
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .updateSelectedUsers(let users):
actionsSubject.send(.updateSelectedUsers(users))
case .openRoom(let identifier):
actionsSubject.send(.openRoom(withIdentifier: identifier))
case .updateDetails(let details):
actionsSubject.send(.updateDetails(details))
case .displayCameraPicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
case .displayMediaPicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
case .removeImage:
actionsSubject.send(.removeImage)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(CreateRoomScreen(context: viewModel.context))
}
}
@@ -0,0 +1,62 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct CreateRoomScreenCoordinatorParameters {
let userSession: UserSessionProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let appSettings: AppSettings
let analytics: AnalyticsService
}
enum CreateRoomScreenCoordinatorAction {
case createdRoom(JoinedRoomProxyProtocol)
case displayMediaPickerWithMode(MediaPickerScreenMode)
}
final class CreateRoomScreenCoordinator: CoordinatorProtocol {
private var viewModel: CreateRoomScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<CreateRoomScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<CreateRoomScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: CreateRoomScreenCoordinatorParameters) {
viewModel = CreateRoomScreenViewModel(userSession: parameters.userSession,
analytics: parameters.analytics,
userIndicatorController: parameters.userIndicatorController,
appSettings: parameters.appSettings)
}
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .createdRoom(let roomProxy):
actionsSubject.send(.createdRoom(roomProxy))
case .displayCameraPicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .camera, selectionType: .single)))
case .displayMediaPicker:
actionsSubject.send(.displayMediaPickerWithMode(.init(source: .photoLibrary, selectionType: .single)))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(CreateRoomScreen(context: viewModel.context))
}
func updateAvatar(fileURL: URL) {
viewModel.updateAvatar(fileURL: fileURL)
}
}
@@ -16,28 +16,24 @@ enum CreateRoomScreenErrorType: Error {
case unknown
}
enum CreateRoomViewModelAction {
case openRoom(withIdentifier: String)
case updateSelectedUsers([UserProfileProxy])
case updateDetails(CreateRoomFlowParameters)
enum CreateRoomScreenViewModelAction {
case createdRoom(JoinedRoomProxyProtocol)
case displayMediaPicker
case displayCameraPicker
case removeImage
}
struct CreateRoomViewState: BindableState {
struct CreateRoomScreenViewState: BindableState {
var roomName: String
let serverName: String
let isKnockingFeatureEnabled: Bool
var selectedUsers: [UserProfileProxy]
var aliasLocalPart: String
var bindings: CreateRoomViewStateBindings
var bindings: CreateRoomScreenViewStateBindings
var avatarURL: URL?
var canCreateRoom: Bool {
!roomName.isEmpty && aliasErrors.isEmpty
}
var aliasErrors: Set<CreateRoomAliasErrorState> = []
var aliasErrors: Set<CreateRoomScreenAliasErrorState> = []
var aliasErrorDescription: String? {
if aliasErrors.contains(.alreadyExists) {
L10n.errorRoomAddressAlreadyExists
@@ -49,7 +45,7 @@ struct CreateRoomViewState: BindableState {
}
}
struct CreateRoomViewStateBindings {
struct CreateRoomScreenViewStateBindings {
var roomTopic: String
var isRoomPrivate: Bool
var isKnockingOnly: Bool
@@ -59,9 +55,8 @@ struct CreateRoomViewStateBindings {
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
}
enum CreateRoomViewAction {
enum CreateRoomScreenViewAction {
case createRoom
case deselectUser(UserProfileProxy)
case displayCameraPicker
case displayMediaPicker
case removeImage
@@ -69,12 +64,12 @@ enum CreateRoomViewAction {
case updateAliasLocalPart(String)
}
enum CreateRoomAliasErrorState {
enum CreateRoomScreenAliasErrorState {
case alreadyExists
case invalidSymbols
}
extension Set<CreateRoomAliasErrorState> {
extension Set<CreateRoomScreenAliasErrorState> {
var errorDescription: String? {
if contains(.alreadyExists) {
return L10n.errorRoomAddressAlreadyExists
@@ -10,83 +10,70 @@ import Combine
import MatrixRustSDK
import SwiftUI
typealias CreateRoomViewModelType = StateStoreViewModel<CreateRoomViewState, CreateRoomViewAction>
typealias CreateRoomScreenViewModelType = StateStoreViewModel<CreateRoomScreenViewState, CreateRoomScreenViewAction>
class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol {
class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreenViewModelProtocol {
struct Parameters {
var name = ""
var topic = ""
var isRoomPrivate = true
var isKnockingOnly = false
var avatarImageMedia: MediaInfo?
var aliasLocalPart: String?
}
private let userSession: UserSessionProtocol
private var createRoomParameters: CreateRoomFlowParameters
private var parameters: Parameters
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private var syncNameAndAlias = true
@CancellableTask private var checkAliasAvailabilityTask: Task<Void, Never>?
private var actionsSubject: PassthroughSubject<CreateRoomViewModelAction, Never> = .init()
private var actionsSubject: PassthroughSubject<CreateRoomScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<CreateRoomViewModelAction, Never> {
var actions: AnyPublisher<CreateRoomScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(userSession: UserSessionProtocol,
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
selectedUsers: [UserProfileProxy],
initialParameters: Parameters = .init(),
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
appSettings: AppSettings) {
let parameters = createRoomParameters.value
self.userSession = userSession
self.createRoomParameters = parameters
parameters = initialParameters
mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings)
self.analytics = analytics
self.userIndicatorController = userIndicatorController
let bindings = CreateRoomViewStateBindings(roomTopic: parameters.topic,
isRoomPrivate: parameters.isRoomPrivate,
isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false)
let bindings = CreateRoomScreenViewStateBindings(roomTopic: parameters.topic,
isRoomPrivate: parameters.isRoomPrivate,
isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false)
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
serverName: userSession.clientProxy.userIDServerName ?? "",
isKnockingFeatureEnabled: appSettings.knockingEnabled,
selectedUsers: selectedUsers,
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
bindings: bindings),
super.init(initialViewState: CreateRoomScreenViewState(roomName: parameters.name,
serverName: userSession.clientProxy.userIDServerName ?? "",
isKnockingFeatureEnabled: appSettings.knockingEnabled,
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
bindings: bindings),
mediaProvider: userSession.mediaProvider)
createRoomParameters
.map(\.avatarImageMedia)
.removeDuplicates { $0?.url == $1?.url }
.sink { [weak self] mediaInfo in
self?.createRoomParameters.avatarImageMedia = mediaInfo
switch mediaInfo {
case .image(_, let thumbnailURL, _):
self?.state.avatarURL = thumbnailURL
case nil:
self?.state.avatarURL = nil
default:
break
}
}
.store(in: &cancellables)
setupBindings()
}
// MARK: - Public
override func process(viewAction: CreateRoomViewAction) {
override func process(viewAction: CreateRoomScreenViewAction) {
switch viewAction {
case .createRoom:
Task {
await createRoom()
}
case .deselectUser(let user):
state.selectedUsers.removeAll { $0.userID == user.userID }
actionsSubject.send(.updateSelectedUsers(state.selectedUsers))
Task { await createRoom() }
case .displayCameraPicker:
actionsSubject.send(.displayCameraPicker)
case .displayMediaPicker:
actionsSubject.send(.displayMediaPicker)
case .removeImage:
actionsSubject.send(.removeImage)
parameters.avatarImageMedia = nil
state.avatarURL = nil
case .updateAliasLocalPart(let aliasLocalPart):
state.aliasLocalPart = aliasLocalPart.lowercased()
// If this has been called this means that the user wants a custom address not necessarily reflecting the name
@@ -104,6 +91,32 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
}
}
func updateAvatar(fileURL: URL) {
showLoadingIndicator()
Task { [weak self] in
guard let self else { return }
do {
guard case let .success(maxUploadSize) = await userSession.clientProxy.maxMediaUploadSize else {
MXLog.error("Failed to get max upload size")
userIndicatorController.alertInfo = AlertInfo(id: .init())
return
}
let mediaInfo = try await mediaUploadingPreprocessor.processMedia(at: fileURL, maxUploadSize: maxUploadSize).get()
switch mediaInfo {
case .image(_, let thumbnailURL, _):
parameters.avatarImageMedia = mediaInfo
state.avatarURL = thumbnailURL
default:
break
}
} catch {
userIndicatorController.alertInfo = AlertInfo(id: .init())
}
hideLoadingIndicator()
}
}
// MARK: - Private
private func setupBindings() {
@@ -135,7 +148,6 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
.sink { [weak self] state in
guard let self else { return }
updateParameters(state: state)
actionsSubject.send(.updateDetails(createRoomParameters))
}
.store(in: &cancellables)
@@ -183,15 +195,15 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
.store(in: &cancellables)
}
private func updateParameters(state: CreateRoomViewState) {
createRoomParameters.name = state.roomName
createRoomParameters.topic = state.bindings.roomTopic
createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate
createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly
private func updateParameters(state: CreateRoomScreenViewState) {
parameters.name = state.roomName
parameters.topic = state.bindings.roomTopic
parameters.isRoomPrivate = state.bindings.isRoomPrivate
parameters.isKnockingOnly = state.bindings.isKnockingOnly
if state.isKnockingFeatureEnabled, !state.aliasLocalPart.isEmpty {
createRoomParameters.aliasLocalPart = state.aliasLocalPart
parameters.aliasLocalPart = state.aliasLocalPart
} else {
createRoomParameters.aliasLocalPart = nil
parameters.aliasLocalPart = nil
}
}
@@ -205,8 +217,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
updateParameters(state: state)
// Better to double check the errors also when trying to create the room
if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate {
guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: createRoomParameters.aliasLocalPart,
if state.isKnockingFeatureEnabled, !parameters.isRoomPrivate {
guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: parameters.aliasLocalPart,
serverName: state.serverName),
isRoomAliasFormatValid(alias: canonicalAlias) else {
state.aliasErrors = [.invalidSymbols]
@@ -226,7 +238,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
}
let avatarURL: URL?
if let media = createRoomParameters.avatarImageMedia {
if let media = parameters.avatarImageMedia {
switch await userSession.clientProxy.uploadMedia(media) {
case .success(let url):
avatarURL = URL(string: url)
@@ -251,17 +263,23 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
avatarURL = nil
}
switch await userSession.clientProxy.createRoom(name: createRoomParameters.name,
topic: createRoomParameters.topic.isBlank ? nil : createRoomParameters.topic,
isRoomPrivate: createRoomParameters.isRoomPrivate,
switch await userSession.clientProxy.createRoom(name: parameters.name,
topic: parameters.topic.isBlank ? nil : parameters.topic,
isRoomPrivate: parameters.isRoomPrivate,
// As of right now we don't want to make private rooms with the knock rule
isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly,
userIDs: state.selectedUsers.map(\.userID),
isKnockingOnly: parameters.isRoomPrivate ? false : parameters.isKnockingOnly,
userIDs: [], // The invite users screen is shown next so we don't need to invite anyone right now.
avatarURL: avatarURL,
aliasLocalPart: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.aliasLocalPart) {
case .success(let roomId):
aliasLocalPart: parameters.isRoomPrivate ? nil : parameters.aliasLocalPart) {
case .success(let roomID):
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
title: L10n.commonError,
message: L10n.screenStartChatErrorStartingChat)
return
}
analytics.trackCreatedRoom(isDM: false)
actionsSubject.send(.openRoom(withIdentifier: roomId))
actionsSubject.send(.createdRoom(roomProxy))
case .failure:
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
title: L10n.commonError,
@@ -271,7 +289,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
// MARK: Loading indicator
private static let loadingIndicatorIdentifier = "\(CreateRoomViewModel.self)-Loading"
private static let loadingIndicatorIdentifier = "\(CreateRoomScreenViewModel.self)-Loading"
private func showLoadingIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
@@ -7,9 +7,12 @@
//
import Combine
import Foundation
@MainActor
protocol CreateRoomViewModelProtocol {
var actions: AnyPublisher<CreateRoomViewModelAction, Never> { get }
var context: CreateRoomViewModelType.Context { get }
protocol CreateRoomScreenViewModelProtocol {
var actions: AnyPublisher<CreateRoomScreenViewModelAction, Never> { get }
var context: CreateRoomScreenViewModelType.Context { get }
func updateAvatar(fileURL: URL)
}
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct CreateRoomScreen: View {
@ObservedObject var context: CreateRoomViewModel.Context
@ObservedObject var context: CreateRoomScreenViewModel.Context
@FocusState private var focus: Focus?
private enum Focus {
@@ -52,7 +52,6 @@ struct CreateRoomScreen: View {
.navigationTitle(L10n.screenCreateRoomTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.readFrame($frame)
.alert(item: $context.alertInfo)
.shouldScrollOnKeyboardDidShow(focus == .alias, to: Focus.alias)
}
@@ -109,6 +108,7 @@ struct CreateRoomScreen: View {
}
}
.buttonStyle(.plain)
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomAvatar)
.confirmationDialog("", isPresented: $context.showAttachmentConfirmationDialog) {
Button(L10n.actionTakePhoto) {
context.send(viewAction: .displayCameraPicker)
@@ -116,6 +116,8 @@ struct CreateRoomScreen: View {
Button(L10n.actionChoosePhoto) {
context.send(viewAction: .displayMediaPicker)
}
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.mediaPicker)
if context.viewState.avatarURL != nil {
Button(L10n.actionRemove, role: .destructive) {
context.send(viewAction: .removeImage)
@@ -134,32 +136,9 @@ struct CreateRoomScreen: View {
} header: {
Text(L10n.screenCreateRoomTopicLabel)
.compoundListSectionHeader()
} footer: {
if !context.viewState.selectedUsers.isEmpty {
selectedUsersSection
}
}
}
@State private var frame: CGRect = .zero
@ScaledMetric private var invitedUserCellWidth: CGFloat = 72
private var selectedUsersSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
context.send(viewAction: .deselectUser(user))
}
.frame(width: invitedUserCellWidth)
}
}
.padding(.horizontal, ListRowPadding.horizontal)
.padding(.vertical, 22)
}
.frame(width: frame.width)
}
private var securitySection: some View {
Section {
ListRow(label: .default(title: L10n.screenCreateRoomPrivateOptionTitle,
@@ -223,6 +202,7 @@ struct CreateRoomScreen: View {
context.send(viewAction: .createRoom)
}
.disabled(!context.viewState.canCreateRoom)
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.create)
}
}
}
@@ -232,65 +212,43 @@ struct CreateRoomScreen: View {
struct CreateRoom_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let parameters = CreateRoomFlowParameters()
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init(selectedUsers),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let emtpyViewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let parameters = CreateRoomFlowParameters()
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
return CreateRoomScreenViewModel(userSession: userSession,
initialParameters: .init(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let publicRoomViewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
let parameters = CreateRoomFlowParameters(isRoomPrivate: false)
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
ServiceLocator.shared.settings.knockingEnabled = true
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
return CreateRoomScreenViewModel(userSession: userSession,
initialParameters: .init(isRoomPrivate: false),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let publicRoomInvalidAliasViewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "#:")
ServiceLocator.shared.settings.knockingEnabled = true
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
return CreateRoomScreenViewModel(userSession: userSession,
initialParameters: .init(isRoomPrivate: false, aliasLocalPart: "#:"),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let publicRoomExistingAliasViewModel = {
let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))
clientProxy.isAliasAvailableReturnValue = .success(false)
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "existing")
ServiceLocator.shared.settings.knockingEnabled = true
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
return CreateRoomScreenViewModel(userSession: userSession,
initialParameters: .init(isRoomPrivate: false, aliasLocalPart: "existing"),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static var previews: some View {
@@ -298,27 +256,22 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
CreateRoomScreen(context: viewModel.context)
}
.previewDisplayName("Create Room")
NavigationStack {
CreateRoomScreen(context: emtpyViewModel.context)
}
.previewDisplayName("Create Room without users")
NavigationStack {
CreateRoomScreen(context: publicRoomViewModel.context)
}
.previewDisplayName("Create Public Room")
NavigationStack {
CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context)
}
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { state in
!state.aliasErrors.isEmpty
})
.snapshotPreferences(expect: publicRoomInvalidAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
.previewDisplayName("Create Public Room, invalid alias")
NavigationStack {
CreateRoomScreen(context: publicRoomExistingAliasViewModel.context)
}
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { state in
!state.aliasErrors.isEmpty
})
.snapshotPreferences(expect: publicRoomExistingAliasViewModel.context.$viewState.map { !$0.aliasErrors.isEmpty })
.previewDisplayName("Create Public Room, existing alias")
}
}
@@ -11,8 +11,8 @@ import SwiftUI
struct InviteUsersScreenCoordinatorParameters {
let userSession: UserSessionProtocol
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
let roomType: InviteUsersScreenRoomType
let roomProxy: JoinedRoomProxyProtocol
let isSkippable: Bool
let userDiscoveryService: UserDiscoveryServiceProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let appSettings: AppSettings
@@ -20,7 +20,6 @@ struct InviteUsersScreenCoordinatorParameters {
enum InviteUsersScreenCoordinatorAction {
case dismiss
case proceed(selectedUsers: [UserProfileProxy])
}
final class InviteUsersScreenCoordinator: CoordinatorProtocol {
@@ -34,8 +33,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
init(parameters: InviteUsersScreenCoordinatorParameters) {
viewModel = InviteUsersScreenViewModel(userSession: parameters.userSession,
selectedUsers: parameters.selectedUsers,
roomType: parameters.roomType,
roomProxy: parameters.roomProxy,
isSkippable: parameters.isSkippable,
userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController,
appSettings: parameters.appSettings)
@@ -47,8 +46,6 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
switch action {
case .dismiss:
actionsSubject.send(.dismiss)
case .proceed(let selectedUsers):
actionsSubject.send(.proceed(selectedUsers: selectedUsers))
}
}
.store(in: &cancellables)
@@ -16,7 +16,6 @@ enum InviteUsersScreenErrorType: Error {
enum InviteUsersScreenViewModelAction {
case dismiss
case proceed(selectedUsers: [UserProfileProxy])
}
enum InviteUsersScreenRoomType {
@@ -53,18 +52,18 @@ struct InviteUsersScreenViewState: BindableState {
membershipState[user.userID]
}
let isCreatingRoom: Bool
let isSkippable: Bool
var actionText: String {
if isCreatingRoom {
return selectedUsers.isEmpty ? L10n.actionSkip : L10n.actionNext
if isSkippable, selectedUsers.isEmpty {
L10n.actionSkip
} else {
return L10n.actionInvite
L10n.actionInvite
}
}
var isActionDisabled: Bool {
isCreatingRoom ? false : selectedUsers.isEmpty
isSkippable ? false : selectedUsers.isEmpty
}
}
@@ -13,7 +13,7 @@ import SwiftUI
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
private let roomType: InviteUsersScreenRoomType
private let roomProxy: JoinedRoomProxyProtocol
private let userDiscoveryService: UserDiscoveryServiceProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let appSettings: AppSettings
@@ -25,22 +25,19 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
actionsSubject.eraseToAnyPublisher()
}
private let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?
init(userSession: UserSessionProtocol,
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>?,
roomType: InviteUsersScreenRoomType,
roomProxy: JoinedRoomProxyProtocol,
isSkippable: Bool,
userDiscoveryService: UserDiscoveryServiceProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
appSettings: AppSettings) {
self.roomType = roomType
self.roomProxy = roomProxy
self.userDiscoveryService = userDiscoveryService
self.userIndicatorController = userIndicatorController
self.appSettings = appSettings
self.selectedUsers = selectedUsers
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers?.value ?? [],
isCreatingRoom: roomType.isCreatingRoom),
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: [],
isSkippable: isSkippable),
mediaProvider: userSession.mediaProvider)
setupSubscriptions()
@@ -62,12 +59,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
case .cancel:
actionsSubject.send(.dismiss)
case .proceed:
switch roomType {
case .draft:
actionsSubject.send(.proceed(selectedUsers: state.selectedUsers))
case .room(let roomProxy):
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
}
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
case .toggleUser(let user):
toggleUser(user)
}
@@ -78,7 +70,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private func toggleUser(_ user: UserProfileProxy) {
if state.selectedUsers.contains(user) {
state.scrollToLastID = nil
state.selectedUsers.removeAll(where: { $0.userID == user.userID })
state.selectedUsers.removeAll { $0.userID == user.userID }
} else {
state.scrollToLastID = user.userID
state.selectedUsers.append(user)
@@ -87,15 +79,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private func inviteUsers(_ users: [String], roomProxy: JoinedRoomProxyProtocol) {
if appSettings.enableKeyShareOnInvite {
showLoader(title: L10n.screenRoomDetailsInvitePeoplePreparing,
message: L10n.screenRoomDetailsInvitePeopleDontClose)
showLoadingIndicator(title: L10n.screenRoomDetailsInvitePeoplePreparing, message: L10n.screenRoomDetailsInvitePeopleDontClose)
} else {
showLoader()
showLoadingIndicator()
}
Task {
defer {
hideLoader()
hideLoadingIndicator()
actionsSubject.send(.dismiss)
}
@@ -122,7 +113,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
}
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
showLoader()
showLoadingIndicator()
Task.detached { [members] in
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
@@ -133,7 +124,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
Task { @MainActor in
self.state.membershipState = membershipState
self.hideLoader()
self.hideLoadingIndicator()
}
}
}
@@ -150,25 +141,13 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
self?.fetchUsers()
}
.store(in: &cancellables)
if let selectedUsers {
selectedUsers
.sink { [weak self] users in
self?.state.selectedUsers = users
}
.store(in: &cancellables)
}
}
private func fetchMembersIfNeeded() {
guard case let .room(roomProxy) = roomType else {
return
}
Task {
showLoader()
showLoadingIndicator()
await roomProxy.updateMembers()
hideLoader()
hideLoadingIndicator()
}
roomProxy.membersPublisher
@@ -211,8 +190,8 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private let userIndicatorID = UUID().uuidString
private func showLoader(title: String = L10n.commonLoading,
message: String? = nil) {
private func showLoadingIndicator(title: String = L10n.commonLoading,
message: String? = nil) {
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
type: .modal,
title: title,
@@ -221,18 +200,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
delay: .milliseconds(200))
}
private func hideLoader() {
private func hideLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(userIndicatorID)
}
}
private extension InviteUsersScreenRoomType {
var isCreatingRoom: Bool {
switch self {
case .draft:
return true
case .room:
return false
}
}
}
@@ -32,6 +32,7 @@ struct InviteUsersScreen: View {
accessibilityFocusOnStart: true)
.compoundSearchField()
.alert(item: $context.alertInfo)
.navigationBarBackButtonHidden(context.viewState.isSkippable)
}
// MARK: - Private
@@ -126,7 +127,7 @@ struct InviteUsersScreen: View {
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
if !context.viewState.isCreatingRoom {
if !context.viewState.isSkippable {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
@@ -155,8 +156,8 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
return InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
selectedUsers: .init([]),
roomType: .draft,
roomProxy: JoinedRoomProxyMock(.init()),
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
@@ -10,38 +10,26 @@ import Combine
import SwiftUI
struct StartChatScreenCoordinatorParameters {
let orientationManager: OrientationManagerProtocol
let userSession: UserSessionProtocol
let userIndicatorController: UserIndicatorControllerProtocol
weak var navigationStackCoordinator: NavigationStackCoordinator?
let userDiscoveryService: UserDiscoveryServiceProtocol
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
let userIndicatorController: UserIndicatorControllerProtocol
let appSettings: AppSettings
let analytics: AnalyticsService
}
enum StartChatScreenCoordinatorAction {
case close
case openRoom(withIdentifier: String)
case createRoom
case openRoom(roomID: String)
case openRoomDirectorySearch
}
final class StartChatScreenCoordinator: CoordinatorProtocol {
private let parameters: StartChatScreenCoordinatorParameters
private var viewModel: StartChatScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<StartChatScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
private var createRoomParameters = CurrentValueSubject<CreateRoomFlowParameters, Never>(.init())
private var createRoomParametersPublisher: CurrentValuePublisher<CreateRoomFlowParameters, Never> {
createRoomParameters.asCurrentValuePublisher()
}
private let selectedUsers = CurrentValueSubject<[UserProfileProxy], Never>([])
private var selectedUsersPublisher: CurrentValuePublisher<[UserProfileProxy], Never> {
selectedUsers.asCurrentValuePublisher()
}
private let actionsSubject: PassthroughSubject<StartChatScreenCoordinatorAction, Never> = .init()
var actions: AnyPublisher<StartChatScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
@@ -63,10 +51,9 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
case .close:
actionsSubject.send(.close)
case .createRoom:
// before creating a room we select the users we would like to invite in that room
presentInviteUsersScreen()
case .showRoom(let identifier):
actionsSubject.send(.openRoom(withIdentifier: identifier))
actionsSubject.send(.createRoom)
case .showRoom(let roomID):
actionsSubject.send(.openRoom(roomID: roomID))
case .openRoomDirectorySearch:
actionsSubject.send(.openRoomDirectorySearch)
}
@@ -79,126 +66,4 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
func toPresentable() -> AnyView {
AnyView(StartChatScreen(context: viewModel.context))
}
// MARK: - Private
private func presentInviteUsersScreen() {
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: parameters.userSession,
selectedUsers: selectedUsersPublisher,
roomType: .draft,
userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController,
appSettings: parameters.appSettings)
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
fatalError("Not shown in this flow.")
case .proceed(let selectedUsers):
self.selectedUsers.send(selectedUsers)
openCreateRoomScreen()
}
}
.store(in: &cancellables)
parameters.navigationStackCoordinator?.push(coordinator) { [weak self] in
self?.createRoomParameters.send(.init())
self?.selectedUsers.send([])
}
}
private func openCreateRoomScreen() {
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
userIndicatorController: parameters.userIndicatorController,
createRoomParameters: createRoomParametersPublisher,
selectedUsers: selectedUsers.value,
appSettings: parameters.appSettings,
analytics: parameters.analytics)
let coordinator = CreateRoomCoordinator(parameters: createParameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .updateSelectedUsers(let users):
self.selectedUsers.send(users)
case .updateDetails(let details):
self.createRoomParameters.send(details)
case .openRoom(let identifier):
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
case .displayMediaPickerWithMode(let mode):
self.displayMediaPickerWithMode(mode)
case .removeImage:
var parameters = self.createRoomParameters.value
parameters.avatarImageMedia = nil
self.createRoomParameters.send(parameters)
}
}
.store(in: &cancellables)
parameters.navigationStackCoordinator?.push(coordinator)
}
// MARK: - Private
private func displayMediaPickerWithMode(_ mode: MediaPickerScreenMode) {
let stackCoordinator = NavigationStackCoordinator()
let mediaPickerCoordinator = MediaPickerScreenCoordinator(mode: mode,
userIndicatorController: parameters.userIndicatorController,
orientationManager: parameters.orientationManager) { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
case .selectedMediaAtURLs(let urls):
guard urls.count == 1,
let url = urls.first else {
fatalError("Received an invalid number of URLs")
}
processAvatar(from: url)
}
}
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
parameters.navigationStackCoordinator?.setSheetCoordinator(stackCoordinator)
}
private func processAvatar(from url: URL) {
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
showLoadingIndicator()
Task { [weak self] in
guard let self else { return }
do {
guard case let .success(maxUploadSize) = await parameters.userSession.clientProxy.maxMediaUploadSize else {
MXLog.error("Failed to get max upload size")
parameters.userIndicatorController.alertInfo = AlertInfo(id: .init())
return
}
let media = try await parameters.mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize).get()
var parameters = createRoomParameters.value
parameters.avatarImageMedia = media
createRoomParameters.send(parameters)
} catch {
parameters.userIndicatorController.alertInfo = AlertInfo(id: .init())
}
hideLoadingIndicator()
}
}
// MARK: Loading indicator
private static let loadingIndicatorIdentifier = "\(StartChatScreenCoordinator.self)-Loading"
private func showLoadingIndicator() {
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
}
private func hideLoadingIndicator() {
parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
}
@@ -16,7 +16,7 @@ enum StartChatScreenErrorType: Error {
enum StartChatScreenViewModelAction: Equatable {
case close
case createRoom
case showRoom(withIdentifier: String)
case showRoom(roomID: String)
case openRoomDirectorySearch
}
@@ -65,7 +65,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
switch currentDirectRoom {
case .success(.some(let roomId)):
hideLoadingIndicator()
actionsSubject.send(.showRoom(withIdentifier: roomId))
actionsSubject.send(.showRoom(roomID: roomId))
case .success:
hideLoadingIndicator()
state.bindings.selectedUserToInvite = user
@@ -190,7 +190,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
switch await userSession.clientProxy.createDirectRoom(with: user.userID, expectedRoomName: user.displayName) {
case .success(let roomId):
analytics.trackCreatedRoom(isDM: true)
actionsSubject.send(.showRoom(withIdentifier: roomId))
actionsSubject.send(.showRoom(roomID: roomId))
case .failure:
displayError()
}
@@ -205,7 +205,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
private func joinRoomByAddress() {
if case let .addressFound(lastTestedAddress, roomID) = internalRoomAddressState,
lastTestedAddress == state.bindings.roomAddress {
actionsSubject.send(.showRoom(withIdentifier: roomID))
actionsSubject.send(.showRoom(roomID: roomID))
} else if let resolveAliasTask {
// If the task is still running we wait for it to complete and we check the state again
showLoadingIndicator(delay: .milliseconds(250))
@@ -1,19 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
/// This parameters are only used in the create room flow for having persisted informations between screens
struct CreateRoomFlowParameters {
var name = ""
var topic = ""
var isRoomPrivate = true
var isKnockingOnly = false
var avatarImageMedia: MediaInfo?
var aliasLocalPart: String?
}
@@ -648,67 +648,48 @@ class MockScreen: Identifiable {
retainedState.append(coordinator)
coordinator.start()
return navigationStackCoordinator
case .startChat:
let navigationStackCoordinator = NavigationStackCoordinator()
let userDiscoveryMock = UserDiscoveryServiceMock()
userDiscoveryMock.searchProfilesWithReturnValue = .success([])
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@mock:client.com"))))
let parameters: StartChatScreenCoordinatorParameters = .init(orientationManager: OrientationManagerMock(),
userSession: userSession,
userIndicatorController: UserIndicatorControllerMock(),
navigationStackCoordinator: navigationStackCoordinator,
userDiscoveryService: userDiscoveryMock,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics)
let coordinator = StartChatScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .startChatWithSearchResults:
let navigationStackCoordinator = NavigationStackCoordinator()
case .startChatFlow:
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com"))
let userDiscoveryMock = UserDiscoveryServiceMock()
userDiscoveryMock.searchProfilesWithReturnValue = .success([.mockBob, .mockBobby])
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let coordinator = StartChatScreenCoordinator(parameters: .init(orientationManager: OrientationManagerMock(),
userSession: userSession,
userIndicatorController: UserIndicatorControllerMock(),
navigationStackCoordinator: navigationStackCoordinator,
userDiscoveryService: userDiscoveryMock,
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .createRoom:
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .success("!new-room:client.com")
clientProxy.roomForIdentifierClosure = { roomID in .joined(JoinedRoomProxyMock(.init(id: roomID, members: []))) }
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockBob, .mockBobby])
let navigationStackCoordinator = NavigationStackCoordinator()
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com"))
let mockUserSession = UserSessionMock(.init(clientProxy: clientProxy))
let createRoomParameters = CreateRoomFlowParameters()
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
let parameters = CreateRoomCoordinatorParameters(userSession: mockUserSession,
userIndicatorController: UserIndicatorControllerMock(),
createRoomParameters: .init(createRoomParameters),
selectedUsers: .init(selectedUsers),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics)
let coordinator = CreateRoomCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .createRoomNoUsers:
let navigationStackCoordinator = NavigationStackCoordinator()
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com"))
let mockUserSession = UserSessionMock(.init(clientProxy: clientProxy))
let createRoomParameters = CreateRoomFlowParameters()
let parameters = CreateRoomCoordinatorParameters(userSession: mockUserSession,
userIndicatorController: UserIndicatorControllerMock(),
createRoomParameters: .init(createRoomParameters),
selectedUsers: .init([]),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics)
let coordinator = CreateRoomCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
let flowCoordinator = StartChatFlowCoordinator(userDiscoveryService: userDiscoveryService,
navigationStackCoordinator: navigationStackCoordinator,
flowParameters: CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
bugReportService: BugReportServiceMock(.init()),
elementCallService: ElementCallServiceMock(.init()),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
linkMetadataProvider: LinkMetadataProvider(),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
notificationManager: NotificationManagerMock(),
stateMachineFactory: StateMachineFactory()))
flowCoordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .finished(let roomID):
navigationRootCoordinator.setSheetCoordinator(nil)
case .showRoomDirectory:
break // The test doesn't cover this.
}
}
.store(in: &cancellables)
retainedState.append(flowCoordinator)
flowCoordinator.start()
// Use a sheet on top the the placeholder so we can test the dismissal.
navigationRootCoordinator.setSheetCoordinator(navigationStackCoordinator)
return PlaceholderScreenCoordinator(hideBrandChrome: false)
case .createPoll:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new,
@@ -22,8 +22,6 @@ enum UITestsScreenIdentifier: String {
case multipleProvidersAuthenticationFlow
case bugReport
case createPoll
case createRoom
case createRoomNoUsers
case encryptionSettings
case encryptionSettingsOutOfSync
case encryptionReset
@@ -44,8 +42,7 @@ enum UITestsScreenIdentifier: String {
case roomWithUndisclosedPolls
case serverSelection
case sessionVerification
case startChat
case startChatWithSearchResults
case startChatFlow
case userSessionScreen
case userSessionScreenReply
case userSessionSpacesFlow
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1624d4be85be4801b7a407ed8d09c815d98b1eb4422d4fc8095795e3a73cd17
size 144918
oid sha256:27b070d92b8846ab416aeb3964d3b698b15aae6a6a5e03f103546bc00b57a291
size 144359
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d668d5fd1d22b0fc782dcf22c1d674fae87d61b289451887e48011ea2c288a9
size 173173
oid sha256:96418567c90c7e7a42b4a7129cc803ce0f50de55cb14021fe8c9b5042810c4e4
size 171338
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e3e2883875feedec7a243f676918e3d6f4d05826702a8f2c2807fc5a0bc7bdf
size 96282
oid sha256:209bc72d50cf549621b1bb690349290aa9f12fd79501cc3089b1dc0a525495c2
size 95602
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd0da3fb9239fe4f131b4a19057bd49bda67b72ba7caf17d8f86cb9f6a239fc0
size 125320
oid sha256:a9f24fc08ec1f408042f6311ddada15f01413f93806bba7742b417743c8cf011
size 125251
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:27b070d92b8846ab416aeb3964d3b698b15aae6a6a5e03f103546bc00b57a291
size 144359
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96418567c90c7e7a42b4a7129cc803ce0f50de55cb14021fe8c9b5042810c4e4
size 171338
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:209bc72d50cf549621b1bb690349290aa9f12fd79501cc3089b1dc0a525495c2
size 95602
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9f24fc08ec1f408042f6311ddada15f01413f93806bba7742b417743c8cf011
size 125251
@@ -1,33 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import XCTest
@MainActor
class CreateRoomScreenUITests: XCTestCase {
func testLanding() async throws {
let app = Application.launch(.createRoom)
try await app.assertScreenshot()
}
func testLandingWithoutUsers() async throws {
let app = Application.launch(.createRoomNoUsers)
try await app.assertScreenshot()
}
func testLongInputNameText() async throws {
let app = Application.launch(.createRoom)
// typeText sometimes misses letters but it's faster than typing one letter at a time
// repeat the same letter enough times to avoid that but also to work on iPads
app.textFields[A11yIdentifiers.createRoomScreen.roomName].tap()
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText(.init(repeating: "x", count: 200))
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText("\n")
try await app.assertScreenshot()
}
}
@@ -1,34 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import XCTest
@MainActor
class StartChatScreenUITests: XCTestCase {
func testLanding() async throws {
let app = Application.launch(.startChat)
try await app.assertScreenshot()
}
func testSearchWithNoResults() async throws {
let app = Application.launch(.startChat)
let searchField = app.searchFields.firstMatch
searchField.clearAndTypeText("None\n", app: app)
XCTAssert(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0))
try await app.assertScreenshot()
}
func testSearchWithResults() async throws {
let app = Application.launch(.startChatWithSearchResults)
let searchField = app.searchFields.firstMatch
searchField.clearAndTypeText("Bob\n", app: app)
XCTAssertFalse(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0))
XCTAssertEqual(app.collectionViews.firstMatch.cells.count, 2)
try await app.assertScreenshot()
}
}
+73
View File
@@ -0,0 +1,73 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import XCTest
@MainActor
class StartChatTests: XCTestCase {
enum Step {
static let startChat = 1
static let startChatWithResults = 2
static let createRoom = 3
static let createRoomAvatarPicker = 4
static let createRoomFilled = 5
static let inviteUsers = 6
static let inviteUsersWithResults = 7
static let inviteUsersSelectedResults = 8
static let dismissed = 99
}
func testFlow() async throws {
let app = Application.launch(.startChatFlow)
try await app.assertScreenshot(step: Step.startChat)
let startChatSearchField = app.searchFields.firstMatch
startChatSearchField.clearAndTypeText("Bob\n", app: app)
XCTAssertFalse(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0))
XCTAssertEqual(app.collectionViews.firstMatch.cells.count, 2)
try await app.assertScreenshot(step: Step.startChatWithResults)
startChatSearchField.clearAndTypeText("\n", app: app)
app.buttons[A11yIdentifiers.startChatScreen.createRoom].firstMatch.tap()
XCTAssertTrue(app.textFields[A11yIdentifiers.createRoomScreen.roomName].waitForExistence(timeout: 1.0))
try await app.assertScreenshot(step: Step.createRoom)
app.buttons[A11yIdentifiers.createRoomScreen.roomAvatar].tap()
app.popovers.buttons.element(boundBy: 2).tap() // There are 2 buttons with the accessibility identifier, so use the index to get the right one.
let cancelButton = app.buttons["Cancel"]
XCTAssertTrue(cancelButton.waitForExistence(timeout: 1.0))
try await app.assertScreenshot(step: Step.createRoomAvatarPicker)
cancelButton.tap()
// typeText sometimes misses letters but it's faster than typing one letter at a time
// repeat the same letter enough times to avoid that but also to work on iPads
app.textFields[A11yIdentifiers.createRoomScreen.roomName].tap()
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText(.init(repeating: "x", count: 200))
app.textFields[A11yIdentifiers.createRoomScreen.roomName].typeText("\n")
try await app.assertScreenshot(step: Step.createRoomFilled)
app.buttons[A11yIdentifiers.createRoomScreen.create].tap()
XCTAssertTrue(app.buttons[A11yIdentifiers.inviteUsersScreen.proceed].waitForExistence(timeout: 1.0))
try await app.assertScreenshot(step: Step.inviteUsers)
let inviteUsersSearchField = app.searchFields.firstMatch
inviteUsersSearchField.clearAndTypeText("Bob\n", app: app)
let cells = app.collectionViews.firstMatch.cells
XCTAssertEqual(cells.count, 2)
try await app.assertScreenshot(step: Step.inviteUsersWithResults)
cells.element(boundBy: 0).tap()
// Selecting the first member has inserted the horizontal collection view above the list.
// So to select the second member we now need to increment the index by 2 instead of 1.
cells.element(boundBy: 2).tap()
try await app.assertScreenshot(step: Step.inviteUsersSelectedResults)
app.buttons[A11yIdentifiers.inviteUsersScreen.proceed].tap()
try await app.assertScreenshot(step: Step.dismissed)
}
}
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71edb284338fe1a3d118141aa237617536b72a31d13333e7148dd2d33ccff1b1
size 152332
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c6fcc3f0eea9bb87f6b5fa2c5b652496ca44eee846d3231e4c1446a1a3da2e3
size 188761
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7a86c8bd9795786c49bace089439a3e8e0677bae255eba332b2175e89ab3f2b
size 131545
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:660beea7152721b0ba27876b1f7d9e34035c21abc188c3ab3953552655041dca
size 160050
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e8ebe176c64660475fdb46016a66ddaa502d30d1c60f6ccc4a52c05ca5412ed
size 149193
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4afad7f48503b599fb30a0e4c593079249d6c70f64770d53ce63398a89d245af
size 183810
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1f07788317f9ed52b340b6e3233d50be4f82a047e6233d6abc656c6e1ba0c65
size 167782
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9543933266405ba35494e5bac5e1b403b0f388657b36b1689a3f27834dd27133
size 164831
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5201d5b8492dadaa23efd16a5e74ca96e23ce972abe4cd76a20533246a9381af
size 198236
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0f8aff85f5930db29bc103b67ae2ea3bd793e6da755b74b3ec4bc3294a24664
size 2332509
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be0bc089efe8628afef744ec9e171a1d3f54b5ac81f3515369a58237c553da0a
size 196815
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02b9e0a8a3ae860c6afe7541021ec11529fb810f13ed7468ed4fbb87d7277e3e
size 147056
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e2ec6a46ee642ad8235dc6cb777330bdeb8a240138409fa88fa9d1f3cc7516b
size 166249
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abe91d1332e9da181baa07576006a411ad36aa23dcd664341f9c1cf1e28c4a57
size 175690
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:048a07df37bc493d91f67b5e1379ef571db047ab35780b28c23ee1990429846f
size 222243
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:10eb8f7c10d0eb6a493c33fc223a96276897f5a8db969d7e07694c9872b47485
size 122115
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bebd60c5ddaaf7d4af54563587c51f44816ac8fe5a621632a14374ca639b66b
size 114369
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6a481a1eebb2bdc2a51b69da7867d330fd15b465ec66dda9a7775ea81bebc27
size 170910
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0f51cb1e35bb75ae45f2f909e45f809febe00c86f7b7d88e63e9cdfb322043b
size 2458946
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab217745f60435fc6251221d5650a25e4ce00153cf78ed72021e05c666d19703
size 166171
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7899f024d4aa043b2e318ef188bb992301aa02bc4a92fd5b59215e907272bde2
size 88945
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b788bbcc940303b16942de95cfcbb142e04ecc60634e409f091a20cf3fe8cef1
size 116322
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:125138db4c413fa5ffc104694cb5400373d86faee69d0c755e2ffe992211f0ab
size 128890
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47d79566040b9e990cd222dc056d22de86723171364d60a0fdc3592dff9bb48e
size 374177
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3e4caed4cef8b0ad70f747bb98a7e205c0b4f1c3df6083f341a9ac5bebe6e31
size 108047
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63a31b32e399472a8206f234665a11bb929403b5902856b85cd4f19373d0fd0a
size 120156
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3156d4c72c601cfbe7584cd73f516a4a32a95e6149dbd7ef31fdd4bba7d14fbf
size 88278
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8744cb36bbdd2afb7809f2682e00d6a3f2d1f3e2e6dfa329a50e97c76d8e9e63
size 90212
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b97677b7315b1d9298468da64358299ef0674644d3042fe23d9e10396131c92a
size 101718
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd670a1f21b71bf5ddc7d317054145c2a049eb16fa09d6cd6f8be589d5ec45e9
size 112321
@@ -13,38 +13,28 @@ import XCTest
@MainActor
class CreateRoomScreenViewModelTests: XCTestCase {
var viewModel: CreateRoomViewModelProtocol!
var viewModel: CreateRoomScreenViewModelProtocol!
var clientProxy: ClientProxyMock!
var userSession: UserSessionMock!
private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
var context: CreateRoomViewModel.Context {
var context: CreateRoomScreenViewModel.Context {
viewModel.context
}
override func setUpWithError() throws {
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
clientProxy.roomForIdentifierClosure = { roomID in .joined(JoinedRoomProxyMock(.init(id: roomID))) }
userSession = UserSessionMock(.init(clientProxy: clientProxy))
let parameters = CreateRoomFlowParameters()
ServiceLocator.shared.settings.knockingEnabled = true
let viewModel = CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: [.mockAlice, .mockBob, .mockCharlie],
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
let viewModel = CreateRoomScreenViewModel(userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
self.viewModel = viewModel
}
func testDeselectUser() {
XCTAssertFalse(context.viewState.selectedUsers.isEmpty)
XCTAssertEqual(context.viewState.selectedUsers.count, 3)
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
context.send(viewAction: .deselectUser(.mockAlice))
XCTAssertNotEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
}
func testDefaultSecurity() {
XCTAssertTrue(context.viewState.bindings.isRoomPrivate)
}
@@ -65,7 +55,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
// When creating the room.
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .success("1")
let deferred = deferFulfillment(viewModel.actions) { action in
guard case .openRoom("1") = action else { return false }
guard case .createdRoom(let roomProxy) = action, roomProxy.id == "1" else { return false }
return true
}
context.send(viewAction: .createRoom)
@@ -21,7 +21,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
}
func testSelectUser() {
setupWithRoomType(roomType: .draft)
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertTrue(context.viewState.selectedUsers.count == 1)
@@ -29,7 +32,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
}
func testReselectUser() {
setupWithRoomType(roomType: .draft)
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
@@ -39,7 +45,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
}
func testDeselectUser() {
setupWithRoomType(roomType: .draft)
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
roomProxy.inviteUserIDReturnValue = .success(())
setupViewModel(roomProxy: roomProxy, isSkippable: true)
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
context.send(viewAction: .toggleUser(.mockAlice))
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
@@ -52,7 +61,7 @@ class InviteUsersScreenViewModelTests: XCTestCase {
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
roomProxy.inviteUserIDReturnValue = .success(())
setupWithRoomType(roomType: .room(roomProxy: roomProxy))
setupViewModel(roomProxy: roomProxy, isSkippable: false)
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
state.isUserSelected(.mockAlice)
@@ -66,8 +75,6 @@ class InviteUsersScreenViewModelTests: XCTestCase {
switch action {
case .dismiss:
return true
default:
return false
}
}
@@ -77,12 +84,12 @@ class InviteUsersScreenViewModelTests: XCTestCase {
XCTAssertEqual(roomProxy.inviteUserIDReceivedInvocations, [RoomMemberProxyMock.mockAlice.userID])
}
private func setupWithRoomType(roomType: InviteUsersScreenRoomType) {
private func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) {
userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([])
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
selectedUsers: nil,
roomType: roomType,
roomProxy: roomProxy,
isSkippable: isSkippable,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
@@ -56,7 +56,7 @@ class StartChatScreenViewModelTests: XCTestCase {
try await deferredViewState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .showRoom(withIdentifier: "id")
action == .showRoom(roomID: "id")
}
context.send(viewAction: .joinRoomByAddress)
try await deferredAction.fulfill()