diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 987d745a5..6ba3a07d8 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -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") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0fa2c6250..1d68a8887 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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 = ""; }; 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMediaEventsTimelineView.swift; sourceTree = ""; }; + 4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModel.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; @@ -2027,6 +2036,7 @@ 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenCoordinator.swift; sourceTree = ""; }; 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenCoordinator.swift; sourceTree = ""; }; 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTimelineItemModifier.swift; sourceTree = ""; }; + 59EBB553F8EFDD149742DC70 /* SpaceAddRoomsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelProtocol.swift; sourceTree = ""; }; 5A07692536D66E3DA32C4964 /* LogViewerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreen.swift; sourceTree = ""; }; 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = ""; }; 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = ""; }; @@ -2161,6 +2171,7 @@ 74AE4F02A507882743973214 /* ManageAuthorizedSpacesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAuthorizedSpacesScreenViewModelProtocol.swift; sourceTree = ""; }; 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenCoordinator.swift; sourceTree = ""; }; 74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; + 74FCAA90142DFBFA1E3E4216 /* SpaceAddRoomsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenCoordinator.swift; sourceTree = ""; }; 7509AB72755DCC4B4E721B36 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/SAS.strings; sourceTree = ""; }; 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposter.swift; sourceTree = ""; }; 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = ""; }; @@ -2170,6 +2181,7 @@ 76A46ABD27628CB5FC402541 /* Backports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backports.swift; sourceTree = ""; }; 7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = ""; }; 7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = ""; }; + 77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenSelectedItem.swift; sourceTree = ""; }; 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = ""; }; 78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerModifier.swift; sourceTree = ""; }; 787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetModels.swift; sourceTree = ""; }; @@ -2182,8 +2194,10 @@ 7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = ""; }; 7A6D867A7FBB70C6EFDBCBC5 /* AccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTests.swift; sourceTree = ""; }; 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelProtocol.swift; sourceTree = ""; }; + 7AB74D407F0E413E7593B369 /* SpaceAddRoomsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModelTests.swift; sourceTree = ""; }; 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilderHook.swift; sourceTree = ""; }; 7AC1E3FE9B59EA094867863E /* TimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineControllerFactoryProtocol.swift; sourceTree = ""; }; + 7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenModels.swift; sourceTree = ""; }; 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModel.swift; sourceTree = ""; }; 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModel.swift; sourceTree = ""; }; 7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2340,6 +2354,7 @@ 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; 951F277E0585E50AC91987C8 /* DeclineAndBlockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelProtocol.swift; sourceTree = ""; }; 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = ""; }; + 95547A6C69606B5282C765D3 /* RemoveItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveItemButton.swift; sourceTree = ""; }; 95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedPreviewTests.swift; sourceTree = ""; }; 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityProxy.swift; sourceTree = ""; }; @@ -2642,6 +2657,7 @@ CC666DAE98245269775329B2 /* RoomMembersFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinatorTests.swift; sourceTree = ""; }; CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = ""; }; + CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreen.swift; sourceTree = ""; }; CCAA1B97A17A750AC706B112 /* SpaceServiceRoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceRoom.swift; sourceTree = ""; }; CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenViewModel.swift; sourceTree = ""; }; CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; @@ -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 = ""; }; + B555926BD272A2832B393895 /* View */ = { + isa = PBXGroup; + children = ( + CC8E9ECC1118A92FAD5191CD /* SpaceAddRoomsScreen.swift */, + 77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */, + ); + path = View; + sourceTree = ""; + }; B687E3E8C23415A06A3D5C65 /* UserIndicator */ = { isa = PBXGroup; children = ( @@ -6511,6 +6539,18 @@ path = View; sourceTree = ""; }; + EA9EB1A95977581772ACF99A /* SpaceAddRoomsScreen */ = { + isa = PBXGroup; + children = ( + 74FCAA90142DFBFA1E3E4216 /* SpaceAddRoomsScreenCoordinator.swift */, + 7ACFFE7814849BB200CE0969 /* SpaceAddRoomsScreenModels.swift */, + 4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */, + 59EBB553F8EFDD149742DC70 /* SpaceAddRoomsScreenViewModelProtocol.swift */, + B555926BD272A2832B393895 /* View */, + ); + path = SpaceAddRoomsScreen; + sourceTree = ""; + }; 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 */, diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 3188e02dc..dc642814a 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -93,7 +93,7 @@ extension ClientProxyMock { unignoreUserReturnValue = .success(()) trackRecentlyVisitedRoomReturnValue = .success(()) - recentlyVisitedRoomsReturnValue = .success([]) + recentlyVisitedRoomsFilterReturnValue = [] recentConversationCounterpartsReturnValue = [] let mediaLoader = MediaLoaderMock() diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1126cf84d..7aa984d3c 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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! + var addChildToReturnValue: Result! { + get { + if Thread.isMainThread { + return addChildToUnderlyingReturnValue + } else { + var returnValue: Result? = 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)? + + func addChild(_ childID: String, to spaceID: String) async -> Result { + 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 { diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index 715d3b4d3..704c10586 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -38,6 +38,7 @@ extension SpaceServiceProxyMock { spaceForIdentifierSpaceIDClosure = { spaceID in .success(configuration.topLevelSpaces.first { $0.id == spaceID }) } + addChildToReturnValue = .success(()) } } diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 70a332c0e..b80c0a454 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -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: diff --git a/ElementX/Sources/Other/SwiftUI/Views/RemoveItemButton.swift b/ElementX/Sources/Other/SwiftUI/Views/RemoveItemButton.swift new file mode 100644 index 000000000..d7e17ff12 --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/RemoveItemButton.swift @@ -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()) + } +} diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 8825293ff..77002e714 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -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, diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift index 78ce349ed..e7b171e56 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenModels.swift @@ -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? diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift index 1629e1366..36918cc52 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift @@ -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 } } } diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift index 5f29b6657..4a9ff3e04 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift @@ -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 } } diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift index 01fa0596b..a42a4886b 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift @@ -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) } } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift index 1ffb8d0d7..28b3b05af 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift @@ -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) { diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift new file mode 100644 index 000000000..48b55b540 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift @@ -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 = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + 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() + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenModels.swift new file mode 100644 index 000000000..204713155 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenModels.swift @@ -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)) + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift new file mode 100644 index 000000000..976cf8a9c --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift @@ -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 + +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 = .init() + var actions: AnyPublisher { + 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.. { get } + var context: SpaceAddRoomsScreenViewModelType.Context { get } + + func stop() +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreen.swift new file mode 100644 index 000000000..e80709faa --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreen.swift @@ -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 + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreenSelectedItem.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreenSelectedItem.swift new file mode 100644 index 000000000..936d15eef --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/View/SpaceAddRoomsScreenSelectedItem.swift @@ -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) + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 56325b190..10f3575e3 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -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 { diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index ac229f483..b38bf7616 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -246,8 +246,7 @@ protocol ClientProxyProtocol: AnyObject { func trackRecentlyVisitedRoom(_ roomID: String) async -> Result - func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> - + func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol] func recentConversationCounterparts() async -> [UserProfileProxy] // MARK: - Crypto diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift index c764c12fe..2ebaad179 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift @@ -67,6 +67,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { } } + func addChild(_ childID: String, to spaceID: String) async -> Result { + 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]) { diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift index bdfff973b..b5c598b07 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift @@ -23,4 +23,7 @@ protocol SpaceServiceProxyProtocol { func leaveSpace(spaceID: String) async -> Result /// 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 } diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 9c9514947..ce688acf1 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -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) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-en-GB.png new file mode 100644 index 000000000..2a6548256 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c8a17622e1808e4e04595ae735f1a816b006f7e93fe6ce206012cf70a3973ba +size 95295 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-pseudo.png new file mode 100644 index 000000000..eba4a59ed --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77ba4947cce3c29021ae45097310ff21b042db8a78df79fbb63361ea0f0a92ee +size 96407 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-en-GB.png new file mode 100644 index 000000000..43a90d79d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bb157b40a5b3c661e8ad56e8d4cebdb99d617fd5a2df3dfb789dfc6617593d2 +size 47349 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-pseudo.png new file mode 100644 index 000000000..20067e96c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Searching-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d1a65e7c5c35361fd5432973ee4e0e27d2d4469a2fbbe2c8313945e58faf59b +size 48140 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png new file mode 100644 index 000000000..fb240f95d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05f1346428ed2f7d7c74085b5344af4959d94987f3227b7d0249ad3cc66a69d0 +size 152502 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png new file mode 100644 index 000000000..07c249704 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c05034b8598c10d9eec2abdf3c4530d0a80d35301d30aa8972351fa7e7315f5 +size 157015 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png new file mode 100644 index 000000000..d6a2f5ce7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e48b453eb68a1b00d72821a72db0d4e48c001366db6ee047cb7048ba330878e +size 99439 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png new file mode 100644 index 000000000..fdc5e9494 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Selected-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb9bd700fd06d334cdf3fcae0748949e1b01972e04f39ca3c1b47418a72211cf +size 102327 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-en-GB.png new file mode 100644 index 000000000..e66905000 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b96b85d9a091da9515212b02ecdac23b23f2f4c9e5a1ee8e9bc68378a796cb3 +size 152293 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-pseudo.png new file mode 100644 index 000000000..d782a2b4d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:117598d6b54d6bd68492fd14f091ff3a436ec920d4e8ac62f83ebb899a66a334 +size 156696 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-en-GB.png new file mode 100644 index 000000000..86d62f5da --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c289a72830e07fb6947359554c10287fcd3fbc942eee4ac72feb5c7b89788ab7 +size 97764 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-pseudo.png new file mode 100644 index 000000000..4aab17b54 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.Suggestions-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45d5e8de16f1f09fc35a036fd093bd9704b6dbd1a190d13e79c777f08f0cff63 +size 100909 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-en-GB-0.png deleted file mode 100644 index 44012ec23..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:434c23652179a0f47f34c44a053a38bd3beb1c4096ea2834874bc4b66fc346f6 -size 84974 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-pseudo-0.png deleted file mode 100644 index 2a098bad5..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPad-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21e5aa2415e7c955b9467670cf7b04f558d22f507dc964f23d062de436f5cc40 -size 87164 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-en-GB-0.png deleted file mode 100644 index 67c60f4be..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a74bd068ad6c4b5a8eb75a985ab7eaf1b635775550bd27200be6862787d6b165 -size 37478 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-pseudo-0.png deleted file mode 100644 index ab461f6a5..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreen.iPhone-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a960dab5b3fe7a34ebac4e82547da455b59f4933b92e02c3d5948065112127a -size 39431 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-en-GB-0.png index 3785d24d9..61d072659 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c8cb1e58a5d450687711d0c4d6c50f9d356cb6c13b708afdc60c01ce659da94 -size 82063 +oid sha256:845647b79b7e8a9dbc0c821e255955211c802962a6aeb82d989efa1011b12694 +size 80334 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-pseudo-0.png index 3785d24d9..61d072659 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c8cb1e58a5d450687711d0c4d6c50f9d356cb6c13b708afdc60c01ce659da94 -size 82063 +oid sha256:845647b79b7e8a9dbc0c821e255955211c802962a6aeb82d989efa1011b12694 +size 80334 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-en-GB-0.png index f96514c76..cd368303b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffe5449c6e7cf1ec06cfc0f28384eb518f72428cb6ff1973bb2dc47d940b52b3 -size 40880 +oid sha256:7a9483757bdac9b61b0185d74f4806c2e453f9808c321cf602e7268b2a55ac0d +size 38752 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-pseudo-0.png index f96514c76..cd368303b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/inviteUsersScreenSelectedItem.iPhone-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffe5449c6e7cf1ec06cfc0f28384eb518f72428cb6ff1973bb2dc47d940b52b3 -size 40880 +oid sha256:7a9483757bdac9b61b0185d74f4806c2e453f9808c321cf602e7268b2a55ac0d +size 38752 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-en-GB.png new file mode 100644 index 000000000..f1681e48e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2542f602ecee4f2d327f87cca3656668a11a5dfb8c314f0e025654735fb814f5 +size 154536 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-pseudo.png new file mode 100644 index 000000000..a0f18f8e8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:809cd3553dfc643ead51a82d7d2f117254eeca2c8140a1c733f3c8f9cb636678 +size 164046 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-en-GB.png new file mode 100644 index 000000000..88064d928 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca5a42f19e835b09bf57b8877a1c16f5c0b0224273627bb86319ba34dd829b3a +size 104977 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-pseudo.png new file mode 100644 index 000000000..7f2b2d0b5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Searching-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c5e0790b78b8f5146e8a5a4e13bb4102723cc60833eed5e5e991a729c1fbe3d +size 118489 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-en-GB.png new file mode 100644 index 000000000..f92ffdc81 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cfd4a43c5f818d2784e81eff1635b4c6e34762ce4e0ab9dd5ad300096605473 +size 153553 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-pseudo.png new file mode 100644 index 000000000..75dbedf58 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:258b82ac769425e6d2c046a56211d8aa4db6302d23a836d20b0ce4cffc6852b0 +size 163527 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-en-GB.png new file mode 100644 index 000000000..b40a2477f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ed5f04fd87c815d536186cf1b310d660a46c7834cd541cd49f0c1ffaf44c853 +size 104775 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-pseudo.png new file mode 100644 index 000000000..09c3ab9a8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Selected-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f78431c2ddabed00050be82c2d14dd3ecb03e046fa851fefe9f381571dd31cb +size 119550 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-en-GB.png new file mode 100644 index 000000000..77ecbd58d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1c1b9c2f983ec86c4a71bcb7e7da886a1c7f0bfb6b9186baba6474773d0599f +size 129646 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-pseudo.png new file mode 100644 index 000000000..13ea1c7d0 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd41b949329959d0c082dd5b60c71d6da161172e6f397bc2d8eadda2eb29b35c +size 143822 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-en-GB.png new file mode 100644 index 000000000..3d630c26b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8d108cc02176cb6a19a5fc3232386c6adbda5e1f68a8b1b139773b97188d41c +size 82794 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-pseudo.png new file mode 100644 index 000000000..7554ec66d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreen.Suggested-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f4f391142890de5d409a80aae05256c5a4613fd57ff9f2cdf994d88b6295d48 +size 100348 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-en-GB-0.png new file mode 100644 index 000000000..bb4561983 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6672ff79b23918ee6eb1a748f3419085913e36322e004875f62fbf3d418bbdec +size 94278 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-pseudo-0.png new file mode 100644 index 000000000..bb4561983 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6672ff79b23918ee6eb1a748f3419085913e36322e004875f62fbf3d418bbdec +size 94278 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-en-GB-0.png new file mode 100644 index 000000000..8942a665c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6340eba7de896583a74ef9d1522401266448423657ddcea3f5ce39d3214de9c +size 52513 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-pseudo-0.png new file mode 100644 index 000000000..8942a665c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceAddRoomsScreenSelectedItem.iPhone-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6340eba7de896583a74ef9d1522401266448423657ddcea3f5ce39d3214de9c +size 52513 diff --git a/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift b/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift new file mode 100644 index 000000000..0301ab78a --- /dev/null +++ b/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift @@ -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()) + } +}