Implement the SpaceAddRoomsScreen. (#4951)

* Update proxies ready for adding rooms to a space.

- ClientProxy.recentlyVisitedRooms to take a filter and to return room proxies.
- Expose SpaceServiceProxy.addChildToSpace.

* Implement the SpaceAddRoomsScreen.

* Fix the designs of the selected carousels.

Add more snapshot tests for inviting users.

* Fix for screen using wrong avatar size.
This commit is contained in:
Doug
2026-01-14 12:07:57 +00:00
committed by GitHub
parent 530e249eee
commit cde9714a83
61 changed files with 1060 additions and 102 deletions
@@ -623,6 +623,14 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "SoftLogoutScreen_Previews")
}
func testSpaceAddRoomsScreenSelectedItem() async throws {
try await performAccessibilityAudit(named: "SpaceAddRoomsScreenSelectedItem_Previews")
}
func testSpaceAddRoomsScreen() async throws {
try await performAccessibilityAudit(named: "SpaceAddRoomsScreen_Previews")
}
func testSpaceHeaderTopicSheetView() async throws {
try await performAccessibilityAudit(named: "SpaceHeaderTopicSheetView_Previews")
}
+48
View File
@@ -45,6 +45,7 @@
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */; };
053B8BD2496207838878C6C9 /* PinnedItemsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C9BAE9F9436B14E4E22E8F /* PinnedItemsBannerView.swift */; };
059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; };
059A6BEDE9BADF3C81AC4146 /* SpaceAddRoomsScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */; };
05BAB510CBC2ED35C154ADD0 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */; };
05E797C4E0048BB487E5C4D6 /* AppLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */; };
05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
@@ -112,6 +113,7 @@
11A6B8E3CBDBF0A4107FF4CE /* OnboardingFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */; };
11D2FDD22DDF34F8323489B9 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; };
1224084B7E289E0830BA2C54 /* VoiceMessageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A293D06BAB2B7A17D9314B /* VoiceMessageRoomTimelineView.swift */; };
12691EC114262874D4A97E23 /* SpaceAddRoomsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */; };
126CBCF5B0145FA1377C1316 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; };
126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; };
128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */; };
@@ -431,6 +433,7 @@
4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; };
4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */; };
4CB30D9BD72CA3FEEFAA0793 /* NSEUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9651CD1066F239C7739240 /* NSEUserSession.swift */; };
4CDA93528027E4C0EB51972C /* SpaceAddRoomsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */; };
4CE638FD837ED72CD98AD9A9 /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; };
4D0F4385B7DDB68C66C78857 /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258C9C815272911A5B132C3 /* FormattedBodyText.swift */; };
4D23D41B8109E010304050F8 /* QRCodeLoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */; };
@@ -669,6 +672,7 @@
75AD7C09BD604A68E2FAA1D9 /* OIDCConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */; };
75ED4B73983228BB6922CE3C /* KnockRequestsListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */; };
761EA50B2619307AB30891B8 /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; };
7624B61D0A3EFEC69C666609 /* SpaceAddRoomsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */; };
762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */; };
762DB0973865293F0C3D3D7B /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */; };
763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; };
@@ -768,6 +772,7 @@
866FA35E7A2339EF8B6D91CA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6B6FFCE5B28AA03DD46F46 /* LinkPreviewView.swift */; };
86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */; };
8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
8696659B8905EE4693B96EAA /* RemoveItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95547A6C69606B5282C765D3 /* RemoveItemButton.swift */; };
86DFA58FBBEB0AF671D2A1E1 /* HomeScreenKnockedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */; };
86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; };
872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; };
@@ -1029,12 +1034,14 @@
B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; };
B444F9C184A377C1B481F07F /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; };
B466827F3766FF8E0CD0D34F /* LinkNewDeviceScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA49F233E193590274FD19F /* LinkNewDeviceScreenCoordinator.swift */; };
B46727C9A79D8D846FF17C45 /* SpaceAddRoomsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FCAA90142DFBFA1E3E4216 /* SpaceAddRoomsScreenCoordinator.swift */; };
B47213F07A67CE3A8D49CEC9 /* TimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC1E3FE9B59EA094867863E /* TimelineControllerFactoryProtocol.swift */; };
B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; };
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; };
B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */; };
B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; };
B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */; };
B5B68C7511DE1E36A89D353A /* SpaceAddRoomsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */; };
B5BCE012F9E7C45D1C76108E /* RoomMembersListScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */; };
B5C40DCFFDFBA0F86E228602 /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = FFA423B0A7BBD8AA9BB91AB0 /* Clocks */; };
B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; };
@@ -1416,6 +1423,7 @@
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 */; };
F9E5FF20B50705EB13137778 /* SpaceAddRoomsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59EBB553F8EFDD149742DC70 /* SpaceAddRoomsScreenViewModelProtocol.swift */; };
F9EA79092C18A8CFE4922DD2 /* PollFormScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */; };
FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; };
FA53FA227FFBE469AFF32F71 /* TimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C585CE1F721A2770C70D47 /* TimelineControllerProtocol.swift */; };
@@ -1905,6 +1913,7 @@
42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = "<group>"; };
42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = "<group>"; };
430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMediaEventsTimelineView.swift; sourceTree = "<group>"; };
4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModel.swift; sourceTree = "<group>"; };
434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = "<group>"; };
43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = "<group>"; };
@@ -2027,6 +2036,7 @@
595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenCoordinator.swift; sourceTree = "<group>"; };
596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenCoordinator.swift; sourceTree = "<group>"; };
59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTimelineItemModifier.swift; sourceTree = "<group>"; };
59EBB553F8EFDD149742DC70 /* SpaceAddRoomsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
5A07692536D66E3DA32C4964 /* LogViewerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreen.swift; sourceTree = "<group>"; };
5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = "<group>"; };
5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = "<group>"; };
@@ -2161,6 +2171,7 @@
74AE4F02A507882743973214 /* ManageAuthorizedSpacesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAuthorizedSpacesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenCoordinator.swift; sourceTree = "<group>"; };
74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
74FCAA90142DFBFA1E3E4216 /* SpaceAddRoomsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenCoordinator.swift; sourceTree = "<group>"; };
7509AB72755DCC4B4E721B36 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/SAS.strings; sourceTree = "<group>"; };
752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposter.swift; sourceTree = "<group>"; };
753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = "<group>"; };
@@ -2170,6 +2181,7 @@
76A46ABD27628CB5FC402541 /* Backports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backports.swift; sourceTree = "<group>"; };
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenSelectedItem.swift; sourceTree = "<group>"; };
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerModifier.swift; sourceTree = "<group>"; };
787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetModels.swift; sourceTree = "<group>"; };
@@ -2182,8 +2194,10 @@
7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
7A6D867A7FBB70C6EFDBCBC5 /* AccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTests.swift; sourceTree = "<group>"; };
7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelTests.swift; sourceTree = "<group>"; };
7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilderHook.swift; sourceTree = "<group>"; };
7AC1E3FE9B59EA094867863E /* TimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineControllerFactoryProtocol.swift; sourceTree = "<group>"; };
7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenModels.swift; sourceTree = "<group>"; };
7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModel.swift; sourceTree = "<group>"; };
7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModel.swift; sourceTree = "<group>"; };
7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2340,6 +2354,7 @@
9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = "<group>"; };
951F277E0585E50AC91987C8 /* DeclineAndBlockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelProtocol.swift; sourceTree = "<group>"; };
955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = "<group>"; };
95547A6C69606B5282C765D3 /* RemoveItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveItemButton.swift; sourceTree = "<group>"; };
95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedPreviewTests.swift; sourceTree = "<group>"; };
95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = "<group>"; };
964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityProxy.swift; sourceTree = "<group>"; };
@@ -2642,6 +2657,7 @@
CC666DAE98245269775329B2 /* RoomMembersFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinatorTests.swift; sourceTree = "<group>"; };
CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = "<group>"; };
CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreen.swift; sourceTree = "<group>"; };
CCAA1B97A17A750AC706B112 /* SpaceServiceRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceRoom.swift; sourceTree = "<group>"; };
CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenViewModel.swift; sourceTree = "<group>"; };
CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = "<group>"; };
@@ -3687,6 +3703,7 @@
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */,
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */,
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */,
95547A6C69606B5282C765D3 /* RemoveItemButton.swift */,
BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */,
1627F2D56477BD331F6D732C /* RoomHeaderView.swift */,
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */,
@@ -4792,6 +4809,7 @@
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */,
9FE8A35B4AD5256F4B562274 /* SettingsScreenViewModelTests.swift */,
AC43313F21511C853D34544E /* SoftLogoutScreenViewModelTests.swift */,
7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */,
5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */,
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */,
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */,
@@ -5443,6 +5461,7 @@
children = (
BDDD421CD80AD0BCBA035076 /* Common */,
EEFCB022372FE5F306ED9199 /* LeaveSpace */,
EA9EB1A95977581772ACF99A /* SpaceAddRoomsScreen */,
FCF165F4DDB83F3DECFEB57A /* SpaceListScreen */,
C360FCF7418FE3593D5A0CBF /* SpaceScreen */,
55312ACF4155CC5B2054AD75 /* SpaceSettingsScreen */,
@@ -5825,6 +5844,15 @@
path = HomeScreen;
sourceTree = "<group>";
};
B555926BD272A2832B393895 /* View */ = {
isa = PBXGroup;
children = (
CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */,
77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */,
);
path = View;
sourceTree = "<group>";
};
B687E3E8C23415A06A3D5C65 /* UserIndicator */ = {
isa = PBXGroup;
children = (
@@ -6511,6 +6539,18 @@
path = View;
sourceTree = "<group>";
};
EA9EB1A95977581772ACF99A /* SpaceAddRoomsScreen */ = {
isa = PBXGroup;
children = (
74FCAA90142DFBFA1E3E4216 /* SpaceAddRoomsScreenCoordinator.swift */,
7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */,
4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */,
59EBB553F8EFDD149742DC70 /* SpaceAddRoomsScreenViewModelProtocol.swift */,
B555926BD272A2832B393895 /* View */,
);
path = SpaceAddRoomsScreen;
sourceTree = "<group>";
};
EB5B1119B5AD79297F1D49EB /* AccountSettings */ = {
isa = PBXGroup;
children = (
@@ -7640,6 +7680,7 @@
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */,
494970EA811FE4D93AC68482 /* SettingsScreenViewModelTests.swift in Sources */,
C797C0B4CF45C66CD1921252 /* SoftLogoutScreenViewModelTests.swift in Sources */,
7624B61D0A3EFEC69C666609 /* SpaceAddRoomsScreenViewModelTests.swift in Sources */,
920DC020F18ABC88175114D3 /* SpaceListScreenViewModelTests.swift in Sources */,
72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.swift in Sources */,
6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */,
@@ -8316,6 +8357,7 @@
8CFDA5F1562479CB3A34D277 /* RedactedRoomTimelineView.swift in Sources */,
3A2640C52505BAB3D983A92A /* RemotePreference.swift in Sources */,
6A64546ABE648ED9E6DBB459 /* RemoteSettingsHook.swift in Sources */,
8696659B8905EE4693B96EAA /* RemoveItemButton.swift in Sources */,
C413D36D44F89DE63D3ADFA4 /* ReportContentScreen.swift in Sources */,
C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */,
46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */,
@@ -8536,6 +8578,12 @@
F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */,
F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */,
CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */,
12691EC114262874D4A97E23 /* SpaceAddRoomsScreen.swift in Sources */,
B46727C9A79D8D846FF17C45 /* SpaceAddRoomsScreenCoordinator.swift in Sources */,
4CDA93528027E4C0EB51972C /* SpaceAddRoomsScreenModels.swift in Sources */,
059A6BEDE9BADF3C81AC4146 /* SpaceAddRoomsScreenSelectedItem.swift in Sources */,
B5B68C7511DE1E36A89D353A /* SpaceAddRoomsScreenViewModel.swift in Sources */,
F9E5FF20B50705EB13137778 /* SpaceAddRoomsScreenViewModelProtocol.swift in Sources */,
8D9A97E32C6C03B884CBD85A /* SpaceExplorerFlowCoordinator.swift in Sources */,
C8E11A335456FCF94A744E6E /* SpaceFlowCoordinator.swift in Sources */,
BF523D9E12E3C4AABBA2F6CB /* SpaceHeaderTopicSheetView.swift in Sources */,
+1 -1
View File
@@ -93,7 +93,7 @@ extension ClientProxyMock {
unignoreUserReturnValue = .success(())
trackRecentlyVisitedRoomReturnValue = .success(())
recentlyVisitedRoomsReturnValue = .success([])
recentlyVisitedRoomsFilterReturnValue = []
recentConversationCounterpartsReturnValue = []
let mediaLoader = MediaLoaderMock()
@@ -5047,15 +5047,15 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
//MARK: - recentlyVisitedRooms
var recentlyVisitedRoomsUnderlyingCallsCount = 0
var recentlyVisitedRoomsCallsCount: Int {
var recentlyVisitedRoomsFilterUnderlyingCallsCount = 0
var recentlyVisitedRoomsFilterCallsCount: Int {
get {
if Thread.isMainThread {
return recentlyVisitedRoomsUnderlyingCallsCount
return recentlyVisitedRoomsFilterUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = recentlyVisitedRoomsUnderlyingCallsCount
returnValue = recentlyVisitedRoomsFilterUnderlyingCallsCount
}
return returnValue!
@@ -5063,27 +5063,27 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
recentlyVisitedRoomsUnderlyingCallsCount = newValue
recentlyVisitedRoomsFilterUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
recentlyVisitedRoomsUnderlyingCallsCount = newValue
recentlyVisitedRoomsFilterUnderlyingCallsCount = newValue
}
}
}
}
var recentlyVisitedRoomsCalled: Bool {
return recentlyVisitedRoomsCallsCount > 0
var recentlyVisitedRoomsFilterCalled: Bool {
return recentlyVisitedRoomsFilterCallsCount > 0
}
var recentlyVisitedRoomsUnderlyingReturnValue: Result<[String], ClientProxyError>!
var recentlyVisitedRoomsReturnValue: Result<[String], ClientProxyError>! {
var recentlyVisitedRoomsFilterUnderlyingReturnValue: [JoinedRoomProxyProtocol]!
var recentlyVisitedRoomsFilterReturnValue: [JoinedRoomProxyProtocol]! {
get {
if Thread.isMainThread {
return recentlyVisitedRoomsUnderlyingReturnValue
return recentlyVisitedRoomsFilterUnderlyingReturnValue
} else {
var returnValue: Result<[String], ClientProxyError>? = nil
var returnValue: [JoinedRoomProxyProtocol]? = nil
DispatchQueue.main.sync {
returnValue = recentlyVisitedRoomsUnderlyingReturnValue
returnValue = recentlyVisitedRoomsFilterUnderlyingReturnValue
}
return returnValue!
@@ -5091,22 +5091,22 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
recentlyVisitedRoomsUnderlyingReturnValue = newValue
recentlyVisitedRoomsFilterUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
recentlyVisitedRoomsUnderlyingReturnValue = newValue
recentlyVisitedRoomsFilterUnderlyingReturnValue = newValue
}
}
}
}
var recentlyVisitedRoomsClosure: (() async -> Result<[String], ClientProxyError>)?
var recentlyVisitedRoomsFilterClosure: (((JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol])?
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> {
recentlyVisitedRoomsCallsCount += 1
if let recentlyVisitedRoomsClosure = recentlyVisitedRoomsClosure {
return await recentlyVisitedRoomsClosure()
func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol] {
recentlyVisitedRoomsFilterCallsCount += 1
if let recentlyVisitedRoomsFilterClosure = recentlyVisitedRoomsFilterClosure {
return await recentlyVisitedRoomsFilterClosure(filter)
} else {
return recentlyVisitedRoomsReturnValue
return recentlyVisitedRoomsFilterReturnValue
}
}
//MARK: - recentConversationCounterparts
@@ -17060,6 +17060,76 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
return joinedParentsChildIDReturnValue
}
}
//MARK: - addChild
var addChildToUnderlyingCallsCount = 0
var addChildToCallsCount: Int {
get {
if Thread.isMainThread {
return addChildToUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = addChildToUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
addChildToUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
addChildToUnderlyingCallsCount = newValue
}
}
}
}
var addChildToCalled: Bool {
return addChildToCallsCount > 0
}
var addChildToReceivedArguments: (childID: String, spaceID: String)?
var addChildToReceivedInvocations: [(childID: String, spaceID: String)] = []
var addChildToUnderlyingReturnValue: Result<Void, SpaceServiceProxyError>!
var addChildToReturnValue: Result<Void, SpaceServiceProxyError>! {
get {
if Thread.isMainThread {
return addChildToUnderlyingReturnValue
} else {
var returnValue: Result<Void, SpaceServiceProxyError>? = nil
DispatchQueue.main.sync {
returnValue = addChildToUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
addChildToUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
addChildToUnderlyingReturnValue = newValue
}
}
}
}
var addChildToClosure: ((String, String) async -> Result<Void, SpaceServiceProxyError>)?
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> {
addChildToCallsCount += 1
addChildToReceivedArguments = (childID: childID, spaceID: spaceID)
DispatchQueue.main.async {
self.addChildToReceivedInvocations.append((childID: childID, spaceID: spaceID))
}
if let addChildToClosure = addChildToClosure {
return await addChildToClosure(childID, spaceID)
} else {
return addChildToReturnValue
}
}
}
class SpaceServiceRoomMock: SpaceServiceRoomProtocol, @unchecked Sendable {
var id: String {
@@ -38,6 +38,7 @@ extension SpaceServiceProxyMock {
spaceForIdentifierSpaceIDClosure = { spaceID in
.success(configuration.topLevelSpaces.first { $0.id == spaceID })
}
addChildToReturnValue = .success(())
}
}
+9 -6
View File
@@ -66,6 +66,7 @@ enum UserAvatarSizeOnScreen {
case settings
case roomDetails
case roomMembersList
case roomChangeRoles
case dmDetails
case startChat
case memberDetails
@@ -110,12 +111,14 @@ enum UserAvatarSizeOnScreen {
return 44
case .roomMembersList:
return 32
case .roomChangeRoles:
return 56
case .startChat:
return 36
case .memberDetails:
return 96
case .inviteUsers:
return 56
return 52
case .editUserDetails:
return 96
case .dmDetails:
@@ -153,6 +156,8 @@ enum RoomAvatarSizeOnScreen {
case roomDirectorySearch
case joinRoom
case spaceHeader
case spaceAddRooms
case spaceAddRoomsSelected
case completionSuggestions
var value: CGFloat {
@@ -164,12 +169,10 @@ enum RoomAvatarSizeOnScreen {
return 32
case .notificationSettings:
return 30
case .messageForwarding:
return 36
case .globalSearch:
return 36
case .roomSelection:
case .messageForwarding, .globalSearch, .roomSelection, .spaceAddRooms:
return 36
case .spaceAddRoomsSelected:
return 52
case .details:
return 96
case .joinRoom:
@@ -0,0 +1,48 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
extension View {
func overlayRemoveItemButton(action: @escaping () -> Void) -> some View {
modifier(RemoveItemButtonViewModifier(action: action))
}
}
private struct RemoveItemButtonViewModifier: ViewModifier {
let action: () -> Void
func body(content: Content) -> some View {
content
.mask {
Rectangle()
.fill(.white)
.overlay(alignment: .topTrailing) {
closeButtonLabel
.hidden()
.padding(2)
.overlay { Circle().fill(.black) }
.offset(x: 2, y: -2)
}
.compositingGroup()
.luminanceToAlpha()
}
.overlay(alignment: .topTrailing) {
Button(action: action) {
closeButtonLabel
}
}
}
var closeButtonLabel: some View {
CompoundIcon(\.close, size: .custom(12), relativeTo: .compound.bodySM)
.foregroundStyle(.compound.iconOnSolidPrimary)
.padding(2)
.background(.compound.iconPrimary, in: Circle())
}
}
@@ -163,6 +163,8 @@ enum TestablePreviewsDictionary {
"SettingsScreen_Previews" : SettingsScreen_Previews.self,
"ShimmerOverlay_Previews" : ShimmerOverlay_Previews.self,
"SoftLogoutScreen_Previews" : SoftLogoutScreen_Previews.self,
"SpaceAddRoomsScreenSelectedItem_Previews" : SpaceAddRoomsScreenSelectedItem_Previews.self,
"SpaceAddRoomsScreen_Previews" : SpaceAddRoomsScreen_Previews.self,
"SpaceHeaderTopicSheetView_Previews" : SpaceHeaderTopicSheetView_Previews.self,
"SpaceHeaderView_Previews" : SpaceHeaderView_Previews.self,
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
@@ -37,8 +37,6 @@ struct InviteUsersScreenViewState: BindableState {
!isSearching && usersSection.type == .searchResult && usersSection.users.isEmpty
}
var scrollToLastID: String?
func isUserSelected(_ user: UserProfileProxy) -> Bool {
isUserDisabled(user) || selectedUsers.contains { $0.userID == user.userID }
}
@@ -69,6 +67,7 @@ struct InviteUsersScreenViewState: BindableState {
struct InviteUsersScreenViewStateBindings {
var searchQuery = ""
var selectedUsersPosition: String?
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<InviteUsersScreenErrorType>?
@@ -69,11 +69,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private func toggleUser(_ user: UserProfileProxy) {
if state.selectedUsers.contains(user) {
state.scrollToLastID = nil
state.selectedUsers.removeAll { $0.userID == user.userID }
} else {
state.scrollToLastID = user.userID
state.selectedUsers.append(user)
withElementAnimation(.easeInOut) { state.bindings.selectedUsersPosition = user.userID }
}
}
@@ -101,28 +101,22 @@ struct InviteUsersScreen: View {
}
}
@ScaledMetric private var cellWidth: CGFloat = 72
@ScaledMetric private var selectedUserCellWidth: CGFloat = 80
private var selectedUsersSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollView in
HStack(spacing: 16) {
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
deselect(user)
}
.frame(width: cellWidth)
HStack(spacing: 8) {
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: context.mediaProvider) {
deselect(user)
}
.frame(width: selectedUserCellWidth)
}
.onChange(of: context.viewState.scrollToLastID) { _, lastAddedID in
guard let id = lastAddedID else { return }
withElementAnimation(.easeInOut) {
scrollView.scrollTo(id)
}
}
.padding(.horizontal, 14)
}
.padding(.horizontal, 16)
.scrollTargetLayout()
}
.scrollPosition(id: $context.selectedUsersPosition, anchor: .trailing)
}
@ToolbarContentBuilder
@@ -152,20 +146,54 @@ struct InviteUsersScreen: View {
// MARK: - Previews
struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
return InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
roomProxy: JoinedRoomProxyMock(.init()),
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()
static let viewModel = makeViewModel()
static let searchingViewModel = makeViewModel(searchQuery: "Alice")
static let selectedViewModel = makeViewModel(hasSelection: true)
static var previews: some View {
NavigationStack {
InviteUsersScreen(context: viewModel.context)
}
.previewDisplayName("Suggestions")
.snapshotPreferences(expect: viewModel.context.$viewState.map { !$0.usersSection.users.isEmpty })
NavigationStack {
InviteUsersScreen(context: searchingViewModel.context)
}
.previewDisplayName("Searching")
.snapshotPreferences(expect: searchingViewModel.context.$viewState.map {
$0.usersSection.type == .searchResult && !$0.usersSection.users.isEmpty
})
NavigationStack {
InviteUsersScreen(context: selectedViewModel.context)
}
.previewDisplayName("Selected")
.snapshotPreferences(expect: selectedViewModel.context.$viewState.map { !$0.selectedUsers.isEmpty })
}
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false) -> InviteUsersScreenViewModel {
let clientProxy = ClientProxyMock(.init())
clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose]
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
roomProxy: JoinedRoomProxyMock(.init(members: [])),
isSkippable: true,
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
if let searchQuery {
viewModel.context.searchQuery = searchQuery
}
if hasSelection {
viewModel.state.selectedUsers = [.mockAlice]
}
return viewModel
}
}
@@ -6,6 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct InviteUsersScreenSelectedItem: View {
@@ -14,13 +15,13 @@ struct InviteUsersScreenSelectedItem: View {
let dismissAction: () -> Void
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 10) {
avatar
.accessibilityHidden(true)
Text(user.displayName ?? user.userID)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.accessibilityElement(children: .combine)
@@ -35,15 +36,14 @@ struct InviteUsersScreenSelectedItem: View {
contentID: user.userID,
avatarSize: .user(on: .inviteUsers),
mediaProvider: mediaProvider)
.overlay(alignment: .topTrailing) {
Button(action: dismissAction) {
Image(systemName: "xmark.circle.fill")
.resizable()
.scaledFrame(size: 20)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconOnSolidPrimary, Color.compound.iconPrimary)
}
}
.overlayRemoveItemButton(action: dismissAction)
}
var closeButtonLabel: some View {
CompoundIcon(\.close, size: .custom(12), relativeTo: .compound.bodySM)
.foregroundStyle(.compound.iconOnSolidPrimary)
.padding(2)
.background(.compound.iconPrimary, in: Circle())
}
}
@@ -52,10 +52,10 @@ struct InviteUsersScreenSelectedItem_Previews: PreviewProvider, TestablePreview
static var previews: some View {
ScrollView(.horizontal) {
HStack(spacing: 28) {
HStack(spacing: 8) {
ForEach(people, id: \.userID) { user in
InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 72)
.frame(width: 80)
}
}
}
@@ -42,7 +42,7 @@ struct RoomChangeRolesScreenSelectedItem: View {
LoadableAvatarImage(url: member.avatarURL,
name: member.name,
contentID: member.id,
avatarSize: .user(on: .inviteUsers),
avatarSize: .user(on: .roomChangeRoles),
mediaProvider: mediaProvider)
.accessibilityHidden(true)
.overlay(alignment: .topTrailing) {
@@ -0,0 +1,55 @@
//
// Copyright 2026 Element Creations 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 SpaceAddRoomsScreenCoordinatorParameters {
let spaceRoomListProxy: SpaceRoomListProxyProtocol
let userSession: UserSessionProtocol
let roomSummaryProvider: RoomSummaryProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
enum SpaceAddRoomsScreenCoordinatorAction {
case dismiss
}
final class SpaceAddRoomsScreenCoordinator: CoordinatorProtocol {
private var viewModel: SpaceAddRoomsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<SpaceAddRoomsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<SpaceAddRoomsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: SpaceAddRoomsScreenCoordinatorParameters) {
viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: parameters.spaceRoomListProxy,
userSession: parameters.userSession,
roomSummaryProvider: parameters.roomSummaryProvider,
userIndicatorController: parameters.userIndicatorController)
}
func start() {
viewModel.actions.sink { [weak self] action in
switch action {
case .dismiss:
self?.actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(SpaceAddRoomsScreen(context: viewModel.context))
}
func stop() {
viewModel.stop()
}
}
@@ -0,0 +1,98 @@
//
// Copyright 2026 Element Creations 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
import MatrixRustSDK
enum SpaceAddRoomsScreenViewModelAction {
case dismiss
}
struct SpaceAddRoomsScreenViewState: BindableState {
var roomsSection: Section
var selectedRooms: [SpaceAddRoomsScreenRoom] = []
var bindings = SpaceAddRoomsScreenViewStateBindings()
struct Section {
enum SectionType: Equatable { case searchResults, suggestions }
let type: SectionType
let rooms: [SpaceAddRoomsScreenRoom]
var title: String? {
switch type {
case .searchResults:
return nil
case .suggestions:
return rooms.isEmpty ? nil : L10n.commonSuggestions
}
}
}
}
struct SpaceAddRoomsScreenViewStateBindings {
var searchQuery = ""
var selectedRoomsPosition: String?
}
enum SpaceAddRoomsScreenViewAction {
case cancel
case reachedTop
case reachedBottom
case searchQueryChanged
case toggleRoom(SpaceAddRoomsScreenRoom)
case save
}
struct SpaceAddRoomsScreenRoom: Identifiable, Equatable {
let id: String
let title: String
let description: String
let avatar: RoomAvatar
}
extension SpaceAddRoomsScreenRoom {
init(summary: RoomSummary) {
self.init(id: summary.id,
title: summary.name,
description: summary.roomListDescription,
avatar: summary.avatar)
}
init(roomProxy: JoinedRoomProxyProtocol) {
self.init(id: roomProxy.id,
title: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
description: roomProxy.infoPublisher.value.roomListDescription,
avatar: roomProxy.infoPublisher.value.avatar)
}
}
private extension RoomInfoProxyProtocol {
var roomListDescription: String {
if isDirect {
return canonicalAlias ?? ""
}
if let alias = canonicalAlias {
return alias
}
guard heroes.count > 0 else {
return ""
}
var heroComponents = heroes.compactMap(\.displayName)
let othersCount = Int(activeMembersCount) - heroes.count
if othersCount > 0 {
heroComponents.append(L10n.commonManyMembers(othersCount))
}
return heroComponents.formatted(.list(type: .and))
}
}
@@ -0,0 +1,142 @@
//
// Copyright 2026 Element Creations 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
typealias SpaceAddRoomsScreenViewModelType = StateStoreViewModelV2<SpaceAddRoomsScreenViewState, SpaceAddRoomsScreenViewAction>
class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRoomsScreenViewModelProtocol {
private let spaceRoomListProxy: SpaceRoomListProxyProtocol
private let spaceServiceProxy: SpaceServiceProxyProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var suggestedRooms: [SpaceAddRoomsScreenRoom] = []
private var actionsSubject: PassthroughSubject<SpaceAddRoomsScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<SpaceAddRoomsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(spaceRoomListProxy: SpaceRoomListProxyProtocol,
userSession: UserSessionProtocol,
roomSummaryProvider: RoomSummaryProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.spaceRoomListProxy = spaceRoomListProxy
spaceServiceProxy = userSession.clientProxy.spaceService
self.roomSummaryProvider = roomSummaryProvider
self.userIndicatorController = userIndicatorController
super.init(initialViewState: SpaceAddRoomsScreenViewState(roomsSection: .init(type: .suggestions, rooms: [])),
mediaProvider: userSession.mediaProvider)
roomSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateRooms()
}
.store(in: &cancellables)
Task {
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
suggestedRooms = await userSession.clientProxy
.recentlyVisitedRooms { roomProxy in
!roomProxy.infoPublisher.value.isDirect && !existingRooms.contains { $0.id == roomProxy.id }
}
.map { .init(roomProxy: $0) }
if state.roomsSection.type == .suggestions {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
}
}
}
override func process(viewAction: SpaceAddRoomsScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
case .reachedTop:
updateVisibleRange(edge: .top)
case .reachedBottom:
updateVisibleRange(edge: .bottom)
case .searchQueryChanged where state.bindings.searchQuery.isEmpty:
roomSummaryProvider.setFilter(.all(filters: []))
case .searchQueryChanged:
roomSummaryProvider.setFilter(.search(query: state.bindings.searchQuery))
case .toggleRoom(let room):
toggleRoom(room)
case .save:
Task { await save() }
}
}
func stop() {
// This is a shared provider so we should reset the filtering when we are done with the view
roomSummaryProvider.setFilter(.all(filters: []))
}
// MARK: - Private
private func updateRooms() {
guard !state.bindings.searchQuery.isEmpty else {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
return
}
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
var rooms = [SpaceAddRoomsScreenRoom]()
for summary in roomSummaryProvider.roomListPublisher.value where !summary.isDirect && !existingRooms.contains(where: { $0.id == summary.id }) {
rooms.append(.init(summary: summary))
}
state.roomsSection = .init(type: .searchResults, rooms: rooms)
// rooms crap
}
/// The actual range values don't matter as long as they contain the lower
/// or upper bounds. updateVisibleRange is a hybrid API that powers both
/// sliding sync visible range update and list paginations
/// For lists other than the home screen one we don't care about visible ranges,
/// we just need the respective bounds to be there to trigger a next page load or
/// a reset to just one page
private func updateVisibleRange(edge: UIRectEdge) {
switch edge {
case .top:
roomSummaryProvider.updateVisibleRange(0..<0)
case .bottom:
let roomCount = roomSummaryProvider.roomListPublisher.value.count
roomSummaryProvider.updateVisibleRange(roomCount..<roomCount)
default:
break
}
}
private func toggleRoom(_ room: SpaceAddRoomsScreenRoom) {
if state.selectedRooms.contains(room) {
state.selectedRooms.removeAll { $0.id == room.id }
} else {
state.selectedRooms.append(room)
withElementAnimation(.easeInOut) { state.bindings.selectedRoomsPosition = room.id }
}
}
private func save() async {
for room in state.selectedRooms {
if case .failure(let error) = await spaceServiceProxy.addChild(room.id, to: spaceRoomListProxy.id) {
MXLog.error("Failed adding room to space: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
updateRooms() // Hide any rooms that were already added.
return
}
}
actionsSubject.send(.dismiss)
}
}
@@ -0,0 +1,16 @@
//
// Copyright 2026 Element Creations 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
@MainActor
protocol SpaceAddRoomsScreenViewModelProtocol {
var actions: AnyPublisher<SpaceAddRoomsScreenViewModelAction, Never> { get }
var context: SpaceAddRoomsScreenViewModelType.Context { get }
func stop()
}
@@ -0,0 +1,211 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct SpaceAddRoomsScreen: View {
@Bindable var context: SpaceAddRoomsScreenViewModel.Context
@State private var formWidth = CGFloat.zero
var showTopSection: Bool {
!context.viewState.selectedRooms.isEmpty
}
var body: some View {
Form {
Section {
EmptyView()
} header: {
VStack(alignment: .leading, spacing: 24) {
Text(L10n.screenSpaceAddRoomsRoomAccessDescription)
.font(.compound.bodySM)
.foregroundStyle(.compound.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
if showTopSection {
selectedRoomsSection
.textCase(.none)
.frame(width: formWidth)
.padding(.bottom, -8)
}
}
.listRowInsets(EdgeInsets())
}
if !context.viewState.roomsSection.rooms.isEmpty {
roomsSection
}
}
.compoundList()
.navigationTitle(L10n.actionAddExistingRooms)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.searchController(query: $context.searchQuery, showsCancelButton: false)
.compoundSearchField()
.disableAutocorrection(true)
.onChange(of: context.searchQuery) { context.send(viewAction: .searchQueryChanged) }
.readWidth($formWidth)
}
@ScaledMetric private var selectedRoomCellWidth: CGFloat = 80
private var selectedRoomsSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(context.viewState.selectedRooms, id: \.id) { room in
SpaceAddRoomsScreenSelectedItem(room: room, mediaProvider: context.mediaProvider) {
context.send(viewAction: .toggleRoom(room))
}
.frame(width: selectedRoomCellWidth)
}
}
.padding(.horizontal, 16)
.scrollTargetLayout()
}
.scrollPosition(id: $context.selectedRoomsPosition, anchor: .trailing)
}
private var roomsSection: some View {
Section {
ForEach(context.viewState.roomsSection.rooms) { room in
SpaceAddRoomsListRow(room: room,
isSelected: context.viewState.selectedRooms.contains { $0.id == room.id },
context: context)
}
// Replace these with ScrollView's `scrollPosition` when dropping iOS 16.
} header: {
switch context.viewState.roomsSection.type {
case .searchResults:
emptyRectangle
.onAppear {
context.send(viewAction: .reachedTop)
}
case .suggestions:
if let sectionTitle = context.viewState.roomsSection.title {
Text(sectionTitle)
}
}
} footer: {
if context.viewState.roomsSection.type == .searchResults {
emptyRectangle
.onAppear {
context.send(viewAction: .reachedBottom)
}
}
}
}
/// The greedy size of Rectangle can create an issue with the navigation bar when the search is highlighted, so is best to use a fixed frame instead of hidden() or EmptyView()
private var emptyRectangle: some View {
Rectangle()
.frame(width: 0, height: 0)
.accessibilityHidden(true)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
ToolbarButton(role: .cancel) {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .confirmationAction) {
ToolbarButton(role: .save) {
context.send(viewAction: .save)
}
.disabled(context.viewState.selectedRooms.isEmpty)
}
}
}
private struct SpaceAddRoomsListRow: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let room: SpaceAddRoomsScreenRoom
let isSelected: Bool
let context: SpaceAddRoomsScreenViewModel.Context
var body: some View {
ListRow(label: .avatar(title: room.title,
description: room.description,
icon: avatar),
kind: .multiSelection(isSelected: isSelected) {
context.send(viewAction: .toggleRoom(room))
})
}
@ViewBuilder @MainActor
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
avatarSize: .room(on: .spaceAddRooms),
mediaProvider: context.mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
}
}
// MARK: - Previews
struct SpaceAddRoomsScreen_Previews: PreviewProvider, TestablePreview {
static let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
static let viewModel = makeViewModel()
static let searchingViewModel = makeViewModel(searchQuery: "Foundation")
static let selectedViewModel = makeViewModel(searchQuery: "Foundation", hasSelection: true)
static var previews: some View {
NavigationStack {
SpaceAddRoomsScreen(context: viewModel.context)
}
.previewDisplayName("Suggested")
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.roomsSection).map {
$0.type == .suggestions && !$0.rooms.isEmpty
})
NavigationStack {
SpaceAddRoomsScreen(context: searchingViewModel.context)
}
.previewDisplayName("Searching")
NavigationStack {
SpaceAddRoomsScreen(context: selectedViewModel.context)
}
.previewDisplayName("Selected")
}
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false) -> SpaceAddRoomsScreenViewModel {
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoomMock(.init(isSpace: true))))
let clientProxy = ClientProxyMock(.init())
clientProxy.recentlyVisitedRoomsFilterReturnValue = [
JoinedRoomProxyMock(.init(id: "1", name: "Room Name", canonicalAlias: "#room-name:example.com")),
JoinedRoomProxyMock(.init(id: "2", name: "Room Name", canonicalAlias: "#room-name:example.com")),
JoinedRoomProxyMock(.init(id: "3", name: "Room Name", canonicalAlias: "#room-name:example.com"))
]
let viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
roomSummaryProvider: summaryProvider,
userIndicatorController: UserIndicatorControllerMock())
if let searchQuery {
viewModel.context.searchQuery = searchQuery
viewModel.context.send(viewAction: .searchQueryChanged)
}
if hasSelection {
viewModel.state.selectedRooms = Array(summaryProvider.roomListPublisher.value.prefix(2)).map(SpaceAddRoomsScreenRoom.init)
}
return viewModel
}
}
@@ -0,0 +1,52 @@
//
// 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 Compound
import SwiftUI
struct SpaceAddRoomsScreenSelectedItem: View {
let room: SpaceAddRoomsScreenRoom
let mediaProvider: MediaProviderProtocol?
let dismissAction: () -> Void
var body: some View {
VStack(spacing: 10) {
avatar
.accessibilityHidden(true)
Text(room.title)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.accessibilityElement(children: .combine)
.accessibilityAction(named: L10n.actionRemove, dismissAction)
}
// MARK: - Private
var avatar: some View {
RoomAvatarImage(avatar: room.avatar,
avatarSize: .room(on: .spaceAddRoomsSelected),
mediaProvider: mediaProvider)
.overlayRemoveItemButton(action: dismissAction)
}
}
struct SpaceAddRoomsScreenSelectedItem_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
SpaceAddRoomsScreenSelectedItem(room: .init(id: "",
title: "Selected Room",
description: "#selected:matrix.org",
avatar: .room(id: "",
name: "Selected Room",
avatarURL: .mockMXCAvatar)),
mediaProvider: MediaProviderMock(configuration: .init())) { }
.frame(width: 80)
}
}
@@ -870,20 +870,35 @@ class ClientProxy: ClientProxyProtocol {
}
}
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> {
do {
let result = try await client.getRecentlyVisitedRooms()
return .success(result)
} catch {
MXLog.error("Failed retrieving recently visited rooms with error: \(error)")
return .failure(.sdkError(error))
func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol] {
let maxResultsToReturn = 5
guard case let .success(roomIdentifiers) = await recentlyVisitedRoomIDs() else {
return []
}
var rooms: [JoinedRoomProxyProtocol] = []
for roomID in roomIdentifiers {
guard case let .joined(roomProxy) = await roomForIdentifier(roomID),
filter(roomProxy) else {
continue
}
rooms.append(roomProxy)
if rooms.count >= maxResultsToReturn {
return rooms
}
}
return rooms
}
func recentConversationCounterparts() async -> [UserProfileProxy] {
let maxResultsToReturn = 5
guard case let .success(roomIdentifiers) = await recentlyVisitedRooms() else {
guard case let .success(roomIdentifiers) = await recentlyVisitedRoomIDs() else {
return []
}
@@ -909,6 +924,16 @@ class ClientProxy: ClientProxyProtocol {
return users.elements
}
private func recentlyVisitedRoomIDs() async -> Result<[String], ClientProxyError> {
do {
let result = try await client.getRecentlyVisitedRooms()
return .success(result)
} catch {
MXLog.error("Failed retrieving recently visited rooms with error: \(error)")
return .failure(.sdkError(error))
}
}
// MARK: Moderation & Safety
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError> {
@@ -246,8 +246,7 @@ protocol ClientProxyProtocol: AnyObject {
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError>
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError>
func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol]
func recentConversationCounterparts() async -> [UserProfileProxy]
// MARK: - Crypto
@@ -67,6 +67,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
}
}
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError> {
do {
return try await .success(spaceService.addChildToSpace(childId: childID, spaceId: spaceID))
} catch {
MXLog.error("Failed to add child \(childID) to space \(spaceID)")
return .failure(.sdkError(error))
}
}
// MARK: - Private
private func handleUpdates(_ updates: [SpaceListUpdate]) {
@@ -23,4 +23,7 @@ protocol SpaceServiceProxyProtocol {
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
/// Returns all the parent spaces of a child that user has joined.
func joinedParents(childID: String) async -> Result<[SpaceServiceRoomProtocol], SpaceServiceProxyError>
/// Adds a room (or space) as a child of another space.
func addChild(_ childID: String, to spaceID: String) async -> Result<Void, SpaceServiceProxyError>
}
@@ -935,6 +935,18 @@ extension PreviewTests {
}
}
func testSpaceAddRoomsScreenSelectedItem() async throws {
for (index, preview) in SpaceAddRoomsScreenSelectedItem_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
func testSpaceAddRoomsScreen() async throws {
for (index, preview) in SpaceAddRoomsScreen_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
func testSpaceHeaderTopicSheetView() async throws {
for (index, preview) in SpaceHeaderTopicSheetView_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c8a17622e1808e4e04595ae735f1a816b006f7e93fe6ce206012cf70a3973ba
size 95295
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77ba4947cce3c29021ae45097310ff21b042db8a78df79fbb63361ea0f0a92ee
size 96407
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bb157b40a5b3c661e8ad56e8d4cebdb99d617fd5a2df3dfb789dfc6617593d2
size 47349
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d1a65e7c5c35361fd5432973ee4e0e27d2d4469a2fbbe2c8313945e58faf59b
size 48140
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05f1346428ed2f7d7c74085b5344af4959d94987f3227b7d0249ad3cc66a69d0
size 152502
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c05034b8598c10d9eec2abdf3c4530d0a80d35301d30aa8972351fa7e7315f5
size 157015
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e48b453eb68a1b00d72821a72db0d4e48c001366db6ee047cb7048ba330878e
size 99439
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb9bd700fd06d334cdf3fcae0748949e1b01972e04f39ca3c1b47418a72211cf
size 102327
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b96b85d9a091da9515212b02ecdac23b23f2f4c9e5a1ee8e9bc68378a796cb3
size 152293
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:117598d6b54d6bd68492fd14f091ff3a436ec920d4e8ac62f83ebb899a66a334
size 156696
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c289a72830e07fb6947359554c10287fcd3fbc942eee4ac72feb5c7b89788ab7
size 97764
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45d5e8de16f1f09fc35a036fd093bd9704b6dbd1a190d13e79c777f08f0cff63
size 100909
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:434c23652179a0f47f34c44a053a38bd3beb1c4096ea2834874bc4b66fc346f6
size 84974
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21e5aa2415e7c955b9467670cf7b04f558d22f507dc964f23d062de436f5cc40
size 87164
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a74bd068ad6c4b5a8eb75a985ab7eaf1b635775550bd27200be6862787d6b165
size 37478
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a960dab5b3fe7a34ebac4e82547da455b59f4933b92e02c3d5948065112127a
size 39431
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c8cb1e58a5d450687711d0c4d6c50f9d356cb6c13b708afdc60c01ce659da94
size 82063
oid sha256:845647b79b7e8a9dbc0c821e255955211c802962a6aeb82d989efa1011b12694
size 80334
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c8cb1e58a5d450687711d0c4d6c50f9d356cb6c13b708afdc60c01ce659da94
size 82063
oid sha256:845647b79b7e8a9dbc0c821e255955211c802962a6aeb82d989efa1011b12694
size 80334
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ffe5449c6e7cf1ec06cfc0f28384eb518f72428cb6ff1973bb2dc47d940b52b3
size 40880
oid sha256:7a9483757bdac9b61b0185d74f4806c2e453f9808c321cf602e7268b2a55ac0d
size 38752
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ffe5449c6e7cf1ec06cfc0f28384eb518f72428cb6ff1973bb2dc47d940b52b3
size 40880
oid sha256:7a9483757bdac9b61b0185d74f4806c2e453f9808c321cf602e7268b2a55ac0d
size 38752
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2542f602ecee4f2d327f87cca3656668a11a5dfb8c314f0e025654735fb814f5
size 154536
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:809cd3553dfc643ead51a82d7d2f117254eeca2c8140a1c733f3c8f9cb636678
size 164046
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca5a42f19e835b09bf57b8877a1c16f5c0b0224273627bb86319ba34dd829b3a
size 104977
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5c5e0790b78b8f5146e8a5a4e13bb4102723cc60833eed5e5e991a729c1fbe3d
size 118489
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1cfd4a43c5f818d2784e81eff1635b4c6e34762ce4e0ab9dd5ad300096605473
size 153553
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:258b82ac769425e6d2c046a56211d8aa4db6302d23a836d20b0ce4cffc6852b0
size 163527
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ed5f04fd87c815d536186cf1b310d660a46c7834cd541cd49f0c1ffaf44c853
size 104775
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f78431c2ddabed00050be82c2d14dd3ecb03e046fa851fefe9f381571dd31cb
size 119550
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b1c1b9c2f983ec86c4a71bcb7e7da886a1c7f0bfb6b9186baba6474773d0599f
size 129646
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd41b949329959d0c082dd5b60c71d6da161172e6f397bc2d8eadda2eb29b35c
size 143822
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8d108cc02176cb6a19a5fc3232386c6adbda5e1f68a8b1b139773b97188d41c
size 82794
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f4f391142890de5d409a80aae05256c5a4613fd57ff9f2cdf994d88b6295d48
size 100348
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6672ff79b23918ee6eb1a748f3419085913e36322e004875f62fbf3d418bbdec
size 94278
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6672ff79b23918ee6eb1a748f3419085913e36322e004875f62fbf3d418bbdec
size 94278
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6340eba7de896583a74ef9d1522401266448423657ddcea3f5ce39d3214de9c
size 52513
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6340eba7de896583a74ef9d1522401266448423657ddcea3f5ce39d3214de9c
size 52513
@@ -0,0 +1,58 @@
//
// 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 XCTest
@testable import ElementX
@MainActor
class SpaceAddRoomsScreenViewModelTests: XCTestCase {
var viewModel: SpaceAddRoomsScreenViewModelProtocol!
var context: SpaceAddRoomsScreenViewModelType.Context { viewModel.context }
func testAddingChildRoom() async throws {
setupViewModel()
var deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
message: "The screen should start with some suggestions.") { section in
section.type == .suggestions && !section.rooms.isEmpty
}
try await deferred.fulfill()
deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
message: "The screen should show search results when there's a query.") { section in
section.type == .searchResults && !section.rooms.isEmpty
}
context.searchQuery = "Foundation"
context.send(viewAction: .searchQueryChanged)
try await deferred.fulfill()
let room = try XCTUnwrap(context.viewState.roomsSection.rooms.first)
context.send(viewAction: .toggleRoom(room))
XCTAssertTrue(context.viewState.selectedRooms.contains(room), "The selected room should be shown.")
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .dismiss }
context.send(viewAction: .save)
try await deferredAction.fulfill()
}
func setupViewModel() {
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoomMock(.init(isSpace: true))))
let clientProxy = ClientProxyMock(.init())
clientProxy.recentlyVisitedRoomsFilterReturnValue = .init(repeating: JoinedRoomProxyMock(.init()), count: 5)
viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
roomSummaryProvider: summaryProvider,
userIndicatorController: UserIndicatorControllerMock())
}
}