Add a SpaceScreen for listing rooms and subspaces within a space. (#4412)

This commit is contained in:
Doug
2025-08-14 17:24:20 +01:00
committed by GitHub
parent b34a3dae59
commit 1e5a5b36b2
34 changed files with 756 additions and 93 deletions

View File

@@ -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")
}

View File

@@ -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 = "<group>"; };
08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = "<group>"; };
094F6B21835890B470DF540C /* SpaceScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenCoordinator.swift; sourceTree = "<group>"; };
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
@@ -1569,6 +1577,7 @@
181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreferenceTests.swift; sourceTree = "<group>"; };
18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.swift; sourceTree = "<group>"; };
184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; };
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModelTests.swift; sourceTree = "<group>"; };
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = "<group>"; };
190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = "<group>"; };
196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = "<group>"; };
@@ -1814,6 +1823,7 @@
4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelProtocol.swift; sourceTree = "<group>"; };
4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = "<group>"; };
4552D3466B1453F287223ADA /* SwipeRightAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeRightAction.swift; sourceTree = "<group>"; };
459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModelProtocol.swift; sourceTree = "<group>"; };
45A4B934BA41D6C255900265 /* preview_video.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_video.jpg; sourceTree = "<group>"; };
45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = "<group>"; };
45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = "<group>"; };
@@ -1953,6 +1963,7 @@
638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveQuickLook.swift; sourceTree = "<group>"; };
63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenModels.swift; sourceTree = "<group>"; };
646B50583A2CE6DA67F7739A /* SpaceScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreen.swift; sourceTree = "<group>"; };
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = "<group>"; };
6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = "<group>"; };
64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -2418,6 +2429,7 @@
BEE365C5A4E90ACBE398EFFE /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/SAS.strings; sourceTree = "<group>"; };
BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAvatarImage.swift; sourceTree = "<group>"; };
BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = "<group>"; };
BF8349149254469E24483FB0 /* SpaceScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenViewModel.swift; sourceTree = "<group>"; };
BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreen.swift; sourceTree = "<group>"; };
BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.swift; sourceTree = "<group>"; };
BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
@@ -2430,6 +2442,7 @@
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = "<group>"; };
C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = "<group>"; };
C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = "<group>"; };
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = "<group>"; };
@@ -2687,6 +2700,7 @@
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = "<group>"; };
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = "<group>"; };
F3C7252B3461D06175D975A4 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/SAS.strings; sourceTree = "<group>"; };
F3D94852AD5BB376CBCC3544 /* RoomPreviewProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPreviewProxy.swift; sourceTree = "<group>"; };
F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -6302,6 +6323,14 @@
path = View;
sourceTree = "<group>";
};
FA1D480A302295CFC3582543 /* View */ = {
isa = PBXGroup;
children = (
646B50583A2CE6DA67F7739A /* SpaceScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
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 */,

View File

@@ -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 dont 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 dont 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 dont have chats for this selection";
"screen_roomlist_filter_people_empty_state_title" = "You dont have any DMs yet";

View File

@@ -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 dont 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 dont have chats for this selection

View File

@@ -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<SpaceRoomListProxyPaginationState, Never>
var paginationResponses: [[SpaceRoomProxyProtocol]]
init(spaceRoomProxy: SpaceRoomProxyProtocol,
initialSpaceRooms: [SpaceRoomProxyProtocol] = [],
paginationStateSubject: CurrentValueSubject<SpaceRoomListProxyPaginationState, Never> = .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))
}
}
}

View File

@@ -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))
]
}
}

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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,

View File

@@ -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??)
"<Parent name> space"
case .invite, .knock, .private, .custom, .none:
L10n.commonPrivateSpace
}
}
var spaceDetailsVisibilityIcon: KeyPath<CompoundIcons, Image> {
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: "")])))
]
}
}

View File

@@ -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))
]
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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<AnyCancellable>()
private let actionsSubject: PassthroughSubject<SpaceScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<SpaceScreenCoordinatorAction, Never> {
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))
}
}

View File

@@ -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)
}

View File

@@ -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<SpaceScreenViewState, SpaceScreenViewAction>
class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<SpaceScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<SpaceScreenViewModelAction, Never> {
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.")
}
}
}

View File

@@ -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<SpaceScreenViewModelAction, Never> { get }
var context: SpaceScreenViewModelType.Context { get }
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d89f971df09420eeb3e2393d12851c50da0896366fd914df6e50f8525b1963bc
size 207443

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:80d43f3c8bd54048177b925efe184e6324da54a6c342ed2f5629983a15ec946a
size 223500

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11b7484b43434f0d598cc596e7eaaa9d898d25b3aba4dfe9d05bdc78ea9b8187
size 138715

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5ea7da5f53f16d9d7eba70450c61ef7ec6b93f70148d32b0152ffa6ec211c33
size 164457

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04df1d04edeac89a7321c3e02ed38ee85bfa0946337896a334d89d8c561195ff
size 207862
oid sha256:5e2b6b775f3a198e0a52991a296bc1ad5756af00b67c3e867eac90313e62513a
size 207919

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9eda888ec16f20eb1f116ce198ef518cdeb2795c8c59b674c0d537cca2acdc97
size 237007
oid sha256:f95f1258a7467c48a260c4d9968822f7e4a68f07e664c115f9d6a127c1cd2cf6
size 237067

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2cf45ff8958dfa9af8bd456e0d72c94c70d769461394ec028dcbe120ba714957
size 149449
oid sha256:104cfb4b0707e035efd03a941da0da8a6c01d189e8d1b66ec1ed4ef1e245062c
size 149502

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf1fd488f8022708dcedfd1f367b3cf688e9153e5505c1657810d6f4959b76a0
size 167283
oid sha256:b4fb5143447b96770909c6a0daddb237e64a3df4e0a48b7aa0d3a60e18c7e586
size 167301

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a9a4c1899fffb9c101f87eeeb2413c962f72daddb2bdf10b23c1aa8267a6707
size 241775

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4debd3d362dfdcec1eebbade1159b3cbbafad46e2daacb1511ad2d08c847926e
size 279363

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1cac5e788e68553c910fcf9d10213c025c593763e72e448db359b2a50f06bd0
size 186591

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90a7af41076c73f8fc3671d6402bdc9e93e23ea592c7efa6647242c44541a900
size 206847

View File

@@ -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)
}

View File

@@ -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<SpaceRoomListProxyPaginationState, Never> = .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()))
}
}