diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 096579847..ca1f3ab57 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -583,6 +583,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "SoftLogoutScreen_Previews") } + func testSpaceHeaderView() async throws { + try await performAccessibilityAudit(named: "SpaceHeaderView_Previews") + } + func testSpaceListScreen() async throws { try await performAccessibilityAudit(named: "SpaceListScreen_Previews") } @@ -591,6 +595,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "SpaceRoomCell_Previews") } + func testSpaceScreen() async throws { + try await performAccessibilityAudit(named: "SpaceScreen_Previews") + } + func testSplashScreen() async throws { try await performAccessibilityAudit(named: "SplashScreen_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 13a9caf4c..6e13f0429 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 24B7CD41342C143117ADA768 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B1CC9AA154F4D5435BF60A /* Comparable.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 24C32D7EF94ECF9081638DF6 /* RemotePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A05E85E4872C3221C5C287 /* RemotePreference.swift */; }; + 250D2E8309C8CC48796D41D1 /* SpaceScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */; }; 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */; }; 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */; }; 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; @@ -482,6 +483,7 @@ 5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6569593FA36B22259E806A67 /* AudioRecorderState.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; + 5A58C7C1837D3E5F2C3E8C8C /* SpaceScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8349149254469E24483FB0 /* SpaceScreenViewModel.swift */; }; 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */; }; 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; @@ -526,6 +528,7 @@ 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; }; 63780F9DA06573E38A471ECA /* GenericCallLinkWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */; }; 6386EA3C898AD1A4BC1DC8A5 /* TimelineMediaPreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD40B92FCF20165658296AD /* TimelineMediaPreviewModifier.swift */; }; + 639A0A27383EC655B0E81E95 /* SpaceScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */; }; 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; }; 63DCEBC1DD555E0D645B9E98 /* MapTilerURLBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1033290D99D5BBA1AF3560A /* MapTilerURLBuilderProtocol.swift */; }; 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; }; @@ -580,6 +583,7 @@ 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */; }; 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */; }; 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */; }; + 6CAADDC6318E41C7D7AA9526 /* SpaceScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646B50583A2CE6DA67F7739A /* SpaceScreen.swift */; }; 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; }; 6DC8E43BA04AC2AC4EB2EB97 /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */; }; 6E03A710799E6C65C0AB36BC /* TargetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D829FD8958376614504B18 /* TargetConfiguration.swift */; }; @@ -608,6 +612,7 @@ 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 7254FB2EFDD43BC8BB7A1213 /* SecurityAndPrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AE42C19EDE64B7CB7BE4D0 /* SecurityAndPrivacyScreen.swift */; }; + 72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 738288EAEE235CAC0893AB9E /* ThreadTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9ACDD96F36510C1FC0836B /* ThreadTimelineScreenViewModel.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; @@ -1231,6 +1236,7 @@ E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; }; E9985DCD1B0D026D7E8BF809 /* ServerSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6640DB5B9171D163E6742639 /* ServerSelectionTests.swift */; }; + E9B4742B3D6E103327466513 /* SpaceHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */; }; E9D2ED1C4186931E3D5FDA4E /* QRCodeLoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718D8767035D37E2DB5CC550 /* QRCodeLoginScreenViewModelProtocol.swift */; }; EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; EA2FECCD9E00D9784AC6017D /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; }; @@ -1262,6 +1268,7 @@ EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EED33AFD9334EFD7398707A6 /* VisualListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD529C89924EE32CE307F36F /* VisualListItem.swift */; }; EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; + EF1C1A5D212B14FFCE0E7C83 /* SpaceScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094F6B21835890B470DF540C /* SpaceScreenCoordinator.swift */; }; EF47D802A404A53F15D5D4B6 /* JoinRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */; }; EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; }; EF79B9EFD094C17FBB4942C2 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E6DE144D887A254F4CAF203 /* UserPreference.swift */; }; @@ -1495,6 +1502,7 @@ 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = ""; }; 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = ""; }; + 094F6B21835890B470DF540C /* SpaceScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenCoordinator.swift; sourceTree = ""; }; 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = ""; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; @@ -1569,6 +1577,7 @@ 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreferenceTests.swift; sourceTree = ""; }; 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; + 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModelTests.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = ""; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = ""; }; @@ -1814,6 +1823,7 @@ 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelProtocol.swift; sourceTree = ""; }; 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; 4552D3466B1453F287223ADA /* SwipeRightAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeRightAction.swift; sourceTree = ""; }; + 459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModelProtocol.swift; sourceTree = ""; }; 45A4B934BA41D6C255900265 /* preview_video.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_video.jpg; sourceTree = ""; }; 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = ""; }; 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = ""; }; @@ -1953,6 +1963,7 @@ 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveQuickLook.swift; sourceTree = ""; }; 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenModels.swift; sourceTree = ""; }; + 646B50583A2CE6DA67F7739A /* SpaceScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreen.swift; sourceTree = ""; }; 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = ""; }; 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = ""; }; 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = ""; }; @@ -2418,6 +2429,7 @@ BEE365C5A4E90ACBE398EFFE /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/SAS.strings; sourceTree = ""; }; BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAvatarImage.swift; sourceTree = ""; }; BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = ""; }; + BF8349149254469E24483FB0 /* SpaceScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModel.swift; sourceTree = ""; }; BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreen.swift; sourceTree = ""; }; BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.swift; sourceTree = ""; }; BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; @@ -2430,6 +2442,7 @@ C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = ""; }; + C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = ""; }; C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; @@ -2687,6 +2700,7 @@ F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = ""; }; F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; + F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = ""; }; F3C7252B3461D06175D975A4 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/SAS.strings; sourceTree = ""; }; F3D94852AD5BB376CBCC3544 /* RoomPreviewProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPreviewProxy.swift; sourceTree = ""; }; F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = ""; }; @@ -4531,6 +4545,7 @@ 9FE8A35B4AD5256F4B562274 /* SettingsScreenViewModelTests.swift */, AC43313F21511C853D34544E /* SoftLogoutScreenViewModelTests.swift */, 5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */, + 18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */, 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */, C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */, 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, @@ -5576,6 +5591,7 @@ BDDD421CD80AD0BCBA035076 /* Common */ = { isa = PBXGroup; children = ( + F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */, A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */, ); path = Common; @@ -5696,6 +5712,11 @@ C360FCF7418FE3593D5A0CBF /* SpaceScreen */ = { isa = PBXGroup; children = ( + 094F6B21835890B470DF540C /* SpaceScreenCoordinator.swift */, + C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */, + BF8349149254469E24483FB0 /* SpaceScreenViewModel.swift */, + 459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */, + FA1D480A302295CFC3582543 /* View */, ); path = SpaceScreen; sourceTree = ""; @@ -6302,6 +6323,14 @@ path = View; sourceTree = ""; }; + FA1D480A302295CFC3582543 /* View */ = { + isa = PBXGroup; + children = ( + 646B50583A2CE6DA67F7739A /* SpaceScreen.swift */, + ); + path = View; + sourceTree = ""; + }; FB039572AA54E0690B4051AD /* Common */ = { isa = PBXGroup; children = ( @@ -7228,6 +7257,7 @@ 494970EA811FE4D93AC68482 /* SettingsScreenViewModelTests.swift in Sources */, C797C0B4CF45C66CD1921252 /* SoftLogoutScreenViewModelTests.swift in Sources */, 920DC020F18ABC88175114D3 /* SpaceListScreenViewModelTests.swift in Sources */, + 72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.swift in Sources */, 6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */, 9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */, 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */, @@ -8071,6 +8101,7 @@ F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */, CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */, 8D9A97E32C6C03B884CBD85A /* SpaceExplorerFlowCoordinator.swift in Sources */, + E9B4742B3D6E103327466513 /* SpaceHeaderView.swift in Sources */, 306ADA9D91EE5F0A30B5E500 /* SpaceListScreen.swift in Sources */, C586E1B286BCD8A774DA16B8 /* SpaceListScreenCoordinator.swift in Sources */, F38186A943D078D30BFB90DE /* SpaceListScreenModels.swift in Sources */, @@ -8081,6 +8112,11 @@ 2EAA1B35D9CA24F090F48792 /* SpaceRoomListProxyProtocol.swift in Sources */, C73CF79A578133D3AB7FB83D /* SpaceRoomProxyMock.swift in Sources */, BE8075CA131C5EA3665C9E0D /* SpaceRoomProxyProtocol.swift in Sources */, + 6CAADDC6318E41C7D7AA9526 /* SpaceScreen.swift in Sources */, + EF1C1A5D212B14FFCE0E7C83 /* SpaceScreenCoordinator.swift in Sources */, + 250D2E8309C8CC48796D41D1 /* SpaceScreenModels.swift in Sources */, + 5A58C7C1837D3E5F2C3E8C8C /* SpaceScreenViewModel.swift in Sources */, + 639A0A27383EC655B0E81E95 /* SpaceScreenViewModelProtocol.swift in Sources */, A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */, DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */, DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 1d60bb51f..22ed7c0f3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -278,6 +278,7 @@ "common_signing_out" = "Signing out"; "common_something_went_wrong" = "Something went wrong"; "common_something_went_wrong_message" = "We encountered an issue. Please try again."; +"common_space" = "Space"; "common_starting_chat" = "Starting chat…"; "common_sticker" = "Sticker"; "common_success" = "Success"; @@ -1075,6 +1076,7 @@ "screen_roomlist_filter_favourites_empty_state_title" = "You don’t have favourite chats yet"; "screen_roomlist_filter_invites_empty_state_title" = "You don't have any pending invites."; "screen_roomlist_filter_low_priority" = "Low Priority"; +"screen_roomlist_filter_low_priority_empty_state_title" = "You don’t have any low priority chats yet"; "screen_roomlist_filter_mixed_empty_state_subtitle" = "You can deselect filters in order to see your other chats"; "screen_roomlist_filter_mixed_empty_state_title" = "You don’t have chats for this selection"; "screen_roomlist_filter_people_empty_state_title" = "You don’t have any DMs yet"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index ff17a3a96..9ea63d368 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -624,6 +624,8 @@ internal enum L10n { internal static var commonSomethingWentWrong: String { return L10n.tr("Localizable", "common_something_went_wrong") } /// We encountered an issue. Please try again. internal static var commonSomethingWentWrongMessage: String { return L10n.tr("Localizable", "common_something_went_wrong_message") } + /// Space + internal static var commonSpace: String { return L10n.tr("Localizable", "common_space") } /// Plural format key: "%#@COUNT@" internal static func commonSpaces(_ p1: Int) -> String { return L10n.tr("Localizable", "common_spaces", p1) @@ -2581,6 +2583,8 @@ internal enum L10n { internal static var screenRoomlistFilterInvitesEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_invites_empty_state_title") } /// Low Priority internal static var screenRoomlistFilterLowPriority: String { return L10n.tr("Localizable", "screen_roomlist_filter_low_priority") } + /// You don’t have any low priority chats yet + internal static var screenRoomlistFilterLowPriorityEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_low_priority_empty_state_title") } /// You can deselect filters in order to see your other chats internal static var screenRoomlistFilterMixedEmptyStateSubtitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_mixed_empty_state_subtitle") } /// You don’t have chats for this selection diff --git a/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift b/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift index 662d603f8..c8823ca10 100644 --- a/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift @@ -5,19 +5,43 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import Foundation import MatrixRustSDK extension SpaceRoomListProxyMock { - struct Configuration { + class Configuration { var spaceRoomProxy: SpaceRoomProxyProtocol - var spaceRooms: [SpaceRoomProxyProtocol] = [] + var initialSpaceRooms: [SpaceRoomProxyProtocol] + var paginationStateSubject: CurrentValueSubject + var paginationResponses: [[SpaceRoomProxyProtocol]] + + init(spaceRoomProxy: SpaceRoomProxyProtocol, + initialSpaceRooms: [SpaceRoomProxyProtocol] = [], + paginationStateSubject: CurrentValueSubject = .init(.idle(endReached: true)), + paginationResponses: [[SpaceRoomProxyProtocol]] = []) { + self.spaceRoomProxy = spaceRoomProxy + self.initialSpaceRooms = initialSpaceRooms + self.paginationStateSubject = paginationStateSubject + self.paginationResponses = paginationResponses + } } convenience init(_ configuration: Configuration) { self.init() + let spaceRoomsSubject: CurrentValueSubject<[SpaceRoomProxyProtocol], Never> = .init(configuration.initialSpaceRooms) + spaceRoom = configuration.spaceRoomProxy - spaceRoomsPublisher = .init(configuration.spaceRooms) + spaceRoomsPublisher = spaceRoomsSubject.asCurrentValuePublisher() + paginationStatePublisher = configuration.paginationStateSubject.asCurrentValuePublisher() + + paginateClosure = { + configuration.paginationStateSubject.send(.loading) + try? await Task.sleep(for: .milliseconds(100)) + let newRooms = configuration.paginationResponses.removeFirst() + spaceRoomsSubject.send(spaceRoomsSubject.value + newRooms) + configuration.paginationStateSubject.send(.idle(endReached: configuration.paginationResponses.isEmpty)) + } } } diff --git a/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift index b659e6cc6..0a1ee927c 100644 --- a/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift @@ -46,3 +46,79 @@ extension SpaceRoomProxyMock { state = configuration.state } } + +extension [SpaceRoomProxyProtocol] { + static var mockJoinedSpaces: [SpaceRoomProxyMock] { + [ + SpaceRoomProxyMock(.init(id: "space1", + name: "The Foundation", + isSpace: true, + childrenCount: 1, + joinedMembersCount: 500, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space2", + name: "The Second Foundation", + isSpace: true, + childrenCount: 1, + joinedMembersCount: 100, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space3", + name: "The Galactic Empire", + isSpace: true, + childrenCount: 25000, + joinedMembersCount: 1_000_000_000, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space4", + name: "The Korellians", + isSpace: true, + childrenCount: 27, + joinedMembersCount: 2_000_000, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space5", + name: "The Luminists", + isSpace: true, + childrenCount: 1, + joinedMembersCount: 100_000, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space6", + name: "The Anacreons", + isSpace: true, + childrenCount: 25, + joinedMembersCount: 400_000, + state: .joined)), + SpaceRoomProxyMock(.init(id: "space7", + name: "The Thespians", + isSpace: true, + childrenCount: 15, + joinedMembersCount: 300_000, + state: .joined)) + ] + } + + static var mockSpaceList: [SpaceRoomProxyProtocol] { + makeSpaceRooms(isSpace: true) + makeSpaceRooms(isSpace: false) + } + + private static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] { + let typeName = isSpace ? "Space" : "Room" + + return [ + SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())1:matrix.org", + name: "Company \(typeName)", + isSpace: isSpace)), + SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())2:matrix.org", + name: "Public \(typeName)", + avatarURL: .mockMXCAvatar, + isSpace: isSpace, + joinedMembersCount: 78, + topic: "Discussion on specific topic goes here.", + joinRule: .public)), + SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())3:matrix.org", + name: "Joined \(typeName)", + isSpace: isSpace, + joinedMembersCount: 123, + topic: "Discussion on specific topic goes here.", + state: .joined)) + ] + } +} diff --git a/ElementX/Sources/Mocks/UserProfile+Mock.swift b/ElementX/Sources/Mocks/UserProfile+Mock.swift index 7d2bb1259..c9242e86b 100644 --- a/ElementX/Sources/Mocks/UserProfile+Mock.swift +++ b/ElementX/Sources/Mocks/UserProfile+Mock.swift @@ -25,6 +25,10 @@ extension UserProfileProxy { .init(userID: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil) } + static var mockDan: UserProfileProxy { + .init(userID: "@dan:matrix.org", displayName: "Dan", avatarURL: .mockMXCUserAvatar) + } + static var mockVerbose: UserProfileProxy { .init(userID: "@charlie:matrix.org", displayName: "Charlie is the best display name", avatarURL: nil) } diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index e81e2688d..924480c35 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -71,6 +71,7 @@ enum UserAvatarSizeOnScreen { case readReceipt case readReceiptSheet case editUserDetails + case spaceHeader case completionSuggestions case blockedUsers case knockingUsersBannerStack @@ -91,6 +92,8 @@ enum UserAvatarSizeOnScreen { return 16 case .readReceiptSheet: return 32 + case .spaceHeader: + return 20 case .completionSuggestions: return 32 case .blockedUsers: diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index d4d1aa699..db98f3438 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -153,8 +153,10 @@ enum TestablePreviewsDictionary { "SettingsScreen_Previews" : SettingsScreen_Previews.self, "ShimmerOverlay_Previews" : ShimmerOverlay_Previews.self, "SoftLogoutScreen_Previews" : SoftLogoutScreen_Previews.self, + "SpaceHeaderView_Previews" : SpaceHeaderView_Previews.self, "SpaceListScreen_Previews" : SpaceListScreen_Previews.self, "SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self, + "SpaceScreen_Previews" : SpaceScreen_Previews.self, "SplashScreen_Previews" : SplashScreen_Previews.self, "StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self, "StartChatScreen_Previews" : StartChatScreen_Previews.self, diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift new file mode 100644 index 000000000..19f87401e --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceHeaderView.swift @@ -0,0 +1,195 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct SpaceHeaderView: View { + let spaceRoomProxy: SpaceRoomProxyProtocol + let mediaProvider: MediaProviderProtocol? + + var title: String { spaceRoomProxy.name ?? "" } + + var body: some View { + VStack(spacing: 16) { + RoomAvatarImage(avatar: spaceRoomProxy.avatar, + avatarSize: .room(on: .spaceHeader), + mediaProvider: mediaProvider) + + VStack(spacing: 8) { + Text(title) + .font(.compound.headingLGBold) + .foregroundStyle(.compound.textPrimary) + .multilineTextAlignment(.center) + + spaceDetails + + SpaceHeaderMembersView(heroes: spaceRoomProxy.heroes, + joinedCount: spaceRoomProxy.joinedMembersCount, + mediaProvider: mediaProvider) + } + + if let topic = spaceRoomProxy.topic { + Text(topic) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + .multilineTextAlignment(.center) + .lineLimit(2) + } + } + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .padding(.top, 32) + .padding(.bottom, 24) + .overlay(alignment: .bottom) { + Rectangle() + .fill(Color.compound.borderDisabled) + .frame(height: 1 / UIScreen.main.scale) + } + } + + var spaceDetails: some View { + Label { + Text(L10n.screenSpaceListDetails(spaceDetailsVisibilityTitle, L10n.commonRooms(spaceRoomProxy.childrenCount))) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + .multilineTextAlignment(.center) + } icon: { + CompoundIcon(spaceDetailsVisibilityIcon, size: .small, relativeTo: .compound.bodyLG) + .foregroundStyle(.compound.iconTertiary) + } + } + + var spaceDetailsVisibilityTitle: String { + switch spaceRoomProxy.joinRule { + case .public: + L10n.commonPublicSpace + case .restricted(let rules), .knockRestricted(let rules): + // FIXME: Get this from the rule (falling back to a passed in parent??) + " space" + case .invite, .knock, .private, .custom, .none: + L10n.commonPrivateSpace + } + } + + var spaceDetailsVisibilityIcon: KeyPath { + switch spaceRoomProxy.joinRule { + case .public: + \.public + case .restricted, .knockRestricted: + \.space + case .invite, .knock, .private, .custom, .none: + \.lock + } + } +} + +import MatrixRustSDK + +struct SpaceHeaderMembersView: View { + let heroes: [UserProfileProxy] + let joinedCount: Int + + let mediaProvider: MediaProviderProtocol? + + var body: some View { + if heroes.isEmpty { + Label(title: title) { + CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + } + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .labelStyle(.custom(spacing: 4)) + .padding(.trailing, 8) + .background(.compound.bgSubtleSecondary, in: Capsule()) + } else { + Label(title: title) { + heroesFacePile + } + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .labelStyle(.custom(spacing: 6)) + } + } + + func title() -> Text { + Text("\(joinedCount)") + } + + var heroesFacePile: some View { + HStack(spacing: -8) { + ForEach(heroes.prefix(3).reversed()) { hero in + LoadableAvatarImage(url: hero.avatarURL, + name: hero.displayName, + contentID: hero.userID, + avatarSize: .user(on: .spaceHeader), + mediaProvider: mediaProvider) + .mask { + Circle() + .fill(Color.white) + .overlay { + if hero != heroes.first { + Circle() + .inset(by: -2) + .fill(Color.black) + .offset(x: 12) + } + } + .compositingGroup() + .luminanceToAlpha() + } + } + } + } +} + +struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview { + static let mediaProvider = MediaProviderMock(configuration: .init()) + + static let spaces = makeSpaceRooms() + + static var previews: some View { + VStack(spacing: 0) { + ForEach(spaces, id: \.id) { space in + SpaceHeaderView(spaceRoomProxy: space, mediaProvider: mediaProvider) + } + } + } + + static func makeSpaceRooms() -> [SpaceRoomProxyMock] { + [ + SpaceRoomProxyMock(.init(id: "!space1:matrix.org", + name: "Company Space", + isSpace: true, + childrenCount: 10, + joinedMembersCount: 50)), + SpaceRoomProxyMock(.init(id: "!space2:matrix.org", + name: "Community Space", + avatarURL: .mockMXCAvatar, + isSpace: true, + childrenCount: 20, + joinedMembersCount: 78, + topic: "Description of the space goes right here.", + joinRule: .public)), + SpaceRoomProxyMock(.init(id: "!space3:matrix.org", + name: "Subspace", + isSpace: true, + childrenCount: 30, + joinedMembersCount: 123, + heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose], + topic: ["Description of the space goes right here.", + "Lorem ipsum dolor sit amet consectetur.", + "Leo viverra morbi habitant in.", + "Sem amet enim habitant nibh augue mauris.", + "Interdum mauris ultrices tincidunt proin morbi erat aenean risus nibh.", + "Diam amet sit fermentum vulputate faucibus."].joined(separator: " "), + joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))) + ] + } +} diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift index 77a8bc75b..a176a079d 100644 --- a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift @@ -136,8 +136,7 @@ struct SpaceRoomCellButtonStyle: ButtonStyle { struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview { static let mediaProvider = MediaProviderMock(configuration: .init()) - static let spaces = makeSpaceRooms(isSpace: true) - static let rooms = makeSpaceRooms(isSpace: false) + static let spaces = [SpaceRoomProxyProtocol].mockSpaceList static var previews: some View { VStack(spacing: 0) { @@ -146,34 +145,6 @@ struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview { isSelected: false, mediaProvider: mediaProvider) { _ in } } - ForEach(rooms, id: \.id) { room in - SpaceRoomCell(spaceRoom: room, - isSelected: false, - mediaProvider: mediaProvider) { _ in } - } } } - - static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] { - let name = isSpace ? "Space" : "Room" - - return [ - SpaceRoomProxyMock(.init(id: "!space1:matrix.org", - name: "Company \(name)", - isSpace: isSpace)), - SpaceRoomProxyMock(.init(id: "!space2:matrix.org", - name: "Public \(name)", - avatarURL: .mockMXCAvatar, - isSpace: isSpace, - joinedMembersCount: 78, - topic: "Discussion on specific topic goes here.", - joinRule: .public)), - SpaceRoomProxyMock(.init(id: "!space3:matrix.org", - name: "Joined \(name)", - isSpace: isSpace, - joinedMembersCount: 123, - topic: "Discussion on specific topic goes here.", - state: .joined)) - ] - } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift index a120c4edf..5186e7b4e 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift @@ -28,6 +28,7 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie mediaProvider: userSession.mediaProvider) spaceServiceProxy.joinedSpacesPublisher + .receive(on: DispatchQueue.main) .weakAssign(to: \.state.joinedSpaces, on: self) .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift index 517aedf0b..0312c6c35 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift @@ -105,56 +105,9 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview { static func makeViewModel(counterValue: Int = 0) -> SpaceListScreenViewModel { let clientProxy = ClientProxyMock(.init()) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - let spaceService = SpaceServiceProxyMock( - .init( - joinedSpaces: [ - SpaceRoomProxyMock(.init(id: "space1", - name: "The Foundation", - isSpace: true, - childrenCount: 1, - joinedMembersCount: 500, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space2", - name: "The Second Foundation", - isSpace: true, - childrenCount: 1, - joinedMembersCount: 100, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space3", - name: "The Galactic Empire", - isSpace: true, - childrenCount: 25000, - joinedMembersCount: 1_000_000_000, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space4", - name: "The Korellians", - isSpace: true, - childrenCount: 27, - joinedMembersCount: 2_000_000, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space5", - name: "The Luminists", - isSpace: true, - childrenCount: 1, - joinedMembersCount: 100_000, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space6", - name: "The Anacreons", - isSpace: true, - childrenCount: 25, - joinedMembersCount: 400_000, - state: .joined)), - SpaceRoomProxyMock(.init(id: "space7", - name: "The Thespians", - isSpace: true, - childrenCount: 15, - joinedMembersCount: 300_000, - state: .joined)) - ] - ) - ) - let viewModel = SpaceListScreenViewModel(userSession: userSession, - spaceServiceProxy: spaceService) + let spaceService = SpaceServiceProxyMock(.init(joinedSpaces: .mockJoinedSpaces)) + + let viewModel = SpaceListScreenViewModel(userSession: userSession, spaceServiceProxy: spaceService) return viewModel } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift new file mode 100644 index 000000000..2b3420094 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -0,0 +1,55 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +// periphery:ignore:all - this is just a space remove this comment once generating the final file + +import Combine +import SwiftUI + +struct SpaceScreenCoordinatorParameters { + let spaceRoomListProxy: SpaceRoomListProxyProtocol + let mediaProvider: MediaProviderProtocol +} + +enum SpaceScreenCoordinatorAction { + case selectSpace(SpaceRoomProxyProtocol) +} + +final class SpaceScreenCoordinator: CoordinatorProtocol { + private let parameters: SpaceScreenCoordinatorParameters + private let viewModel: SpaceScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SpaceScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = SpaceScreenViewModel(spaceRoomList: parameters.spaceRoomListProxy, mediaProvider: parameters.mediaProvider) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .selectSpace(let spaceRoom): + actionsSubject.send(.selectSpace(spaceRoom)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(SpaceScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift new file mode 100644 index 000000000..c8348f136 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -0,0 +1,27 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum SpaceScreenViewModelAction { + case selectSpace(SpaceRoomProxyProtocol) +} + +struct SpaceScreenViewState: BindableState { + let space: SpaceRoomProxyProtocol + + var isPaginating = false + var rooms: [SpaceRoomProxyProtocol] + + var bindings = SpaceScreenViewStateBindings() +} + +struct SpaceScreenViewStateBindings { } + +enum SpaceScreenViewAction { + case spaceAction(SpaceRoomCell.Action) +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift new file mode 100644 index 000000000..3e543a906 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -0,0 +1,60 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias SpaceScreenViewModelType = StateStoreViewModelV2 + +class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtocol { + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(spaceRoomList: SpaceRoomListProxyProtocol, mediaProvider: MediaProviderProtocol) { + super.init(initialViewState: SpaceScreenViewState(space: spaceRoomList.spaceRoom, + rooms: spaceRoomList.spaceRoomsPublisher.value), + mediaProvider: mediaProvider) + + spaceRoomList.spaceRoomsPublisher + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.rooms, on: self) + .store(in: &cancellables) + + spaceRoomList.paginationStatePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] paginationState in + switch paginationState { + case .idle(let endReached): + self?.state.isPaginating = false + guard !endReached else { return } + Task { await spaceRoomList.paginate() } + case .loading: + self?.state.isPaginating = true + } + } + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: SpaceScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .spaceAction(.select(let spaceRoom)): + if spaceRoom.isSpace { + actionsSubject.send(.selectSpace(spaceRoom)) + } else { + #warning("Implement joining") + } + case .spaceAction(.join(let spaceID)): + #warning("Implement joining.") + } + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModelProtocol.swift new file mode 100644 index 000000000..3e8e9d7f2 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine + +@MainActor +protocol SpaceScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: SpaceScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift new file mode 100644 index 000000000..9c961a8dd --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -0,0 +1,71 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct SpaceScreen: View { + @Bindable var context: SpaceScreenViewModel.Context + + var body: some View { + ScrollView { + LazyVStack(spacing: 0) { + SpaceHeaderView(spaceRoomProxy: context.viewState.space, + mediaProvider: context.mediaProvider) + rooms + } + } + .navigationTitle(context.viewState.space.name ?? L10n.commonSpace) + .navigationBarTitleDisplayMode(.inline) + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + } + + @ViewBuilder + var rooms: some View { + ForEach(context.viewState.rooms, id: \.id) { spaceRoom in + SpaceRoomCell(spaceRoom: spaceRoom, + isSelected: false, + mediaProvider: context.mediaProvider) { action in + context.send(viewAction: .spaceAction(action)) + } + } + + if context.viewState.isPaginating { + ProgressView() + .padding() + } + } +} + +// MARK: - Previews + +struct SpaceScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = makeViewModel() + + static var previews: some View { + NavigationStack { + SpaceScreen(context: viewModel.context) + } + } + + static func makeViewModel() -> SpaceScreenViewModel { + let spaceRoomProxy = SpaceRoomProxyMock(.init(id: "!eng-space:matrix.org", + name: "Engineering Team", + isSpace: true, + childrenCount: 30, + joinedMembersCount: 76, + heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose], + topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.", + joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))) + let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy, + initialSpaceRooms: .mockSpaceList)) + + let viewModel = SpaceScreenViewModel(spaceRoomList: spaceRoomListProxy, + mediaProvider: MediaProviderMock(configuration: .init())) + return viewModel + } +} diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift index ccdd06516..1bb982785 100644 --- a/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift +++ b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift @@ -9,7 +9,7 @@ import Combine import MatrixRustSDK // Temporary until the SDK is updated. -enum SpaceRoomListProxyPaginationState { +enum SpaceRoomListProxyPaginationState: Equatable { case idle(endReached: Bool) case loading } diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 48ff62a53..5bd341fef 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -875,6 +875,12 @@ extension PreviewTests { } } + func testSpaceHeaderView() async throws { + for (index, preview) in SpaceHeaderView_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testSpaceListScreen() async throws { for (index, preview) in SpaceListScreen_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) @@ -887,6 +893,12 @@ extension PreviewTests { } } + func testSpaceScreen() async throws { + for (index, preview) in SpaceScreen_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testSplashScreen() async throws { for (index, preview) in SplashScreen_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-en-GB-0.png new file mode 100644 index 000000000..77ae2b7e7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d89f971df09420eeb3e2393d12851c50da0896366fd914df6e50f8525b1963bc +size 207443 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-pseudo-0.png new file mode 100644 index 000000000..9a908fa58 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80d43f3c8bd54048177b925efe184e6324da54a6c342ed2f5629983a15ec946a +size 223500 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..826a43d0f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11b7484b43434f0d598cc596e7eaaa9d898d25b3aba4dfe9d05bdc78ea9b8187 +size 138715 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..3ca2eefc6 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceHeaderView.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5ea7da5f53f16d9d7eba70450c61ef7ec6b93f70148d32b0152ffa6ec211c33 +size 164457 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png index 91df5863a..886968f76 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04df1d04edeac89a7321c3e02ed38ee85bfa0946337896a334d89d8c561195ff -size 207862 +oid sha256:5e2b6b775f3a198e0a52991a296bc1ad5756af00b67c3e867eac90313e62513a +size 207919 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png index 5a40b655e..00aa449fb 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eda888ec16f20eb1f116ce198ef518cdeb2795c8c59b674c0d537cca2acdc97 -size 237007 +oid sha256:f95f1258a7467c48a260c4d9968822f7e4a68f07e664c115f9d6a127c1cd2cf6 +size 237067 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png index 74fc929ba..6c7c3458b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cf45ff8958dfa9af8bd456e0d72c94c70d769461394ec028dcbe120ba714957 -size 149449 +oid sha256:104cfb4b0707e035efd03a941da0da8a6c01d189e8d1b66ec1ed4ef1e245062c +size 149502 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png index ff29f11a5..57b2e4241 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf1fd488f8022708dcedfd1f367b3cf688e9153e5505c1657810d6f4959b76a0 -size 167283 +oid sha256:b4fb5143447b96770909c6a0daddb237e64a3df4e0a48b7aa0d3a60e18c7e586 +size 167301 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-en-GB-0.png new file mode 100644 index 000000000..2089680a0 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9a4c1899fffb9c101f87eeeb2413c962f72daddb2bdf10b23c1aa8267a6707 +size 241775 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-pseudo-0.png new file mode 100644 index 000000000..0e6484a66 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4debd3d362dfdcec1eebbade1159b3cbbafad46e2daacb1511ad2d08c847926e +size 279363 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..9ce6057d1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1cac5e788e68553c910fcf9d10213c025c593763e72e448db359b2a50f06bd0 +size 186591 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..c23ebafa5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceScreen.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90a7af41076c73f8fc3671d6402bdc9e93e23ea592c7efa6647242c44541a900 +size 206847 diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index 580284a25..94025a919 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -27,15 +27,19 @@ class SpaceListScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.joinedRoomsCount, 0) } - func testJoinedSpacesSubscription() { + func testJoinedSpacesSubscription() async throws { setupViewModel() + var deferred = deferFulfillment(context.observe(\.viewState.joinedSpaces)) { $0.count == 0 } joinedSpacesSubject.send([]) + try await deferred.fulfill() XCTAssertEqual(context.viewState.joinedSpaces.count, 0) + deferred = deferFulfillment(context.observe(\.viewState.joinedSpaces)) { $0.count == 1 } joinedSpacesSubject.send([ SpaceRoomProxyMock(.init(isSpace: true)) ]) + try await deferred.fulfill() XCTAssertEqual(context.viewState.joinedSpaces.count, 1) } diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift new file mode 100644 index 000000000..ffd2326ca --- /dev/null +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -0,0 +1,117 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import XCTest + +@testable import ElementX + +@MainActor +class SpaceScreenViewModelTests: XCTestCase { + var spaceRoomListProxy: SpaceRoomListProxyMock! + let mockSpaceRooms = [SpaceRoomProxyProtocol].mockSpaceList + var paginationStateSubject: CurrentValueSubject = .init(.idle(endReached: true)) + + var viewModel: SpaceScreenViewModelProtocol! + + var context: SpaceScreenViewModelType.Context { + viewModel.context + } + + func testInitialState() { + setupViewModel() + + XCTAssertFalse(context.viewState.isPaginating) + XCTAssertTrue(context.viewState.rooms.isEmpty) + XCTAssertFalse(spaceRoomListProxy.paginateCalled) + } + + func testSinglePagination() async throws { + // Given a space screen view model for a space with a single paginations worth of children. + let response = mockSpaceRooms.prefix(3) + setupViewModel(paginationResponses: [Array(response)]) + + XCTAssertFalse(context.viewState.isPaginating) + XCTAssertTrue(context.viewState.rooms.isEmpty) + XCTAssertFalse(spaceRoomListProxy.paginateCalled) + XCTAssertFalse(response.isEmpty, "There should be some test rooms.") + + // When the pagination is triggered. + var deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .loading } + paginationStateSubject.send(.idle(endReached: false)) // Invert the default to allow paginate to be called. + try await deferred.fulfill() + + // Then the screen should show a paginating indicator. + XCTAssertTrue(context.viewState.isPaginating) + XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1) + + // When waiting for the pagination to finish. + deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .idle(endReached: true) } + try await deferred.fulfill() + + // Then no more pagination requests should be made the the space rooms should be populated. + XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 1) + XCTAssertEqual(context.viewState.rooms.map(\.id), response.map(\.id)) + } + + func testMultiplePaginations() async throws { + // Given a space screen view model for a space with two distinct paginations worth of children. + let response1 = mockSpaceRooms.prefix(3) + let response2 = mockSpaceRooms.suffix(mockSpaceRooms.count - 3) + setupViewModel(paginationResponses: [Array(response1), Array(response2)]) + + XCTAssertFalse(context.viewState.isPaginating) + XCTAssertTrue(context.viewState.rooms.isEmpty) + XCTAssertFalse(spaceRoomListProxy.paginateCalled) + XCTAssertFalse(response1.isEmpty, "There should be some test rooms.") + XCTAssertFalse(response2.isEmpty, "There should be more test rooms.") + + // When the pagination is triggered. + let deferredIsPaginating = deferFulfillment(context.observe(\.viewState.isPaginating), transitionValues: [true, false, true, false]) + let deferredState = deferFulfillment(spaceRoomListProxy.paginationStatePublisher, keyPath: \.self, transitionValues: [.loading, + .idle(endReached: false), + .loading, + .idle(endReached: true)]) + paginationStateSubject.send(.idle(endReached: false)) // Invert the default to allow paginate to be called. + + // Then the screen should show 2 distinct paginations and finish up with all of the rooms visible. + try await deferredIsPaginating.fulfill() + try await deferredState.fulfill() + + XCTAssertFalse(context.viewState.isPaginating) + XCTAssertEqual(spaceRoomListProxy.paginateCallsCount, 2) + XCTAssertEqual(context.viewState.rooms.map(\.id), mockSpaceRooms.map(\.id)) + } + + func testSelectingSpace() async throws { + setupViewModel() + + let selectedSpace = mockSpaceRooms[0] + let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true } + viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace))) + let action = try await deferred.fulfill() + + switch action { + case .selectSpace(let spaceRoomProxy) where spaceRoomProxy.id == selectedSpace.id: + break + default: + XCTFail("The action should select the space.") + } + } + + // MARK: - Helpers + + private func setupViewModel(paginationResponses: [[SpaceRoomProxyProtocol]] = []) { + spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: SpaceRoomProxyMock(.init(isSpace: true)), + paginationStateSubject: paginationStateSubject, + paginationResponses: paginationResponses)) + + viewModel = SpaceScreenViewModel(spaceRoomList: spaceRoomListProxy, + mediaProvider: MediaProviderMock(configuration: .init())) + } +}