Add SpaceRoomCell & Space…ProxyProtocols and use them on the SpaceListScreen. (#4404)

This commit is contained in:
Doug
2025-08-13 12:06:29 +01:00
committed by GitHub
parent 192f4765c3
commit 3dc6d73899
32 changed files with 781 additions and 39 deletions
@@ -587,6 +587,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "SpaceListScreen_Previews")
}
func testSpaceRoomCell() async throws {
try await performAccessibilityAudit(named: "SpaceRoomCell_Previews")
}
func testSplashScreen() async throws {
try await performAccessibilityAudit(named: "SplashScreen_Previews")
}
+52
View File
@@ -259,6 +259,7 @@
2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; };
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; };
2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; };
2EAA1B35D9CA24F090F48792 /* SpaceRoomListProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */; };
2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; };
2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; };
2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; };
@@ -837,6 +838,7 @@
9AC5F8142413862A9E3A2D98 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; };
9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */; };
9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */; };
9B84F55288AB98783C11CC49 /* SpaceRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */; };
9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */; };
9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */; };
9C11138F7D8C291494BB0B20 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
@@ -859,6 +861,7 @@
9F8BEA86540D8980BDD7C176 /* AuthenticationStartScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3AB4D399D39FBD78119011 /* AuthenticationStartScreenModels.swift */; };
9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; };
9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */; };
9FBF0078657CA4F2A9747E98 /* SpaceRoomListProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */; };
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; };
@@ -872,6 +875,7 @@
A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; };
A1BA8D6BABAFA9BAAEAA3FFD /* NotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */; };
A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */; };
A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */; };
A216C83ADCF32BA5EF8A6FBC /* InviteUsersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */; };
A2172B5A26976F9174228B8A /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; };
A2357AA4A188BC37085BC6F0 /* EditRoomAddressScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */; };
@@ -1015,6 +1019,7 @@
BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */; };
BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; };
BDFF0AEBF57B5B124062DAEF /* GeneratedAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CEB4590CCF70F0E3C0B171 /* GeneratedAccessibilityTests.swift */; };
BE8075CA131C5EA3665C9E0D /* SpaceRoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01F2E9FD8FF95AB89A345E6 /* SpaceRoomProxyProtocol.swift */; };
BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
BEC6DFEA506085D3027E353C /* MediaEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */; };
BED59052E5C5163D2B065CA6 /* EventTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */; };
@@ -1059,6 +1064,7 @@
C5E3A4A678B4F8900830B76A /* NavigationTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C39B1D3FC8CF41D6C3B054F /* NavigationTabCoordinator.swift */; };
C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */; };
C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; };
C73CF79A578133D3AB7FB83D /* SpaceRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF771312D208C8002A2F05C4 /* SpaceRoomProxyMock.swift */; };
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; };
C797C0B4CF45C66CD1921252 /* SoftLogoutScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC43313F21511C853D34544E /* SoftLogoutScreenViewModelTests.swift */; };
@@ -1153,6 +1159,7 @@
DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; };
DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; };
DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; };
DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */; };
DB65401349C143DFF883E2B0 /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EC6EA7EDFCE46710DA306 /* AnalyticsPromptScreenViewModel.swift */; };
DBC3FDE1540B39702A117D8E /* RoomRolesAndPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DDC82DB6A84E700CD5DEC0 /* RoomRolesAndPermissionsTests.swift */; };
DBC8D1DBFE9F9CA7662BC8AA /* RoomPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */; };
@@ -2064,6 +2071,7 @@
7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = "<group>"; };
7D39AF1F659923D77778511E /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxyMock.swift; sourceTree = "<group>"; };
7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModel.swift; sourceTree = "<group>"; };
7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2246,9 +2254,11 @@
A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = "<group>"; };
A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = "<group>"; };
A01F2E9FD8FF95AB89A345E6 /* SpaceRoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomProxyProtocol.swift; sourceTree = "<group>"; };
A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = "<group>"; };
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetFlowCoordinator.swift; sourceTree = "<group>"; };
A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomCell.swift; sourceTree = "<group>"; };
A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenKnockedCell.swift; sourceTree = "<group>"; };
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2327,6 +2337,7 @@
AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = "<group>"; };
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = "<group>"; };
AF2E6ADAE685F4109B1FE795 /* TimelineThreadSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineThreadSummaryView.swift; sourceTree = "<group>"; };
AF771312D208C8002A2F05C4 /* SpaceRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomProxyMock.swift; sourceTree = "<group>"; };
AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = "<group>"; };
AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = "<group>"; };
@@ -2442,6 +2453,7 @@
C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; };
C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderStateTests.swift; sourceTree = "<group>"; };
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyProtocol.swift; sourceTree = "<group>"; };
C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = "<group>"; };
C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = "<group>"; };
C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2626,6 +2638,7 @@
EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModelTests.swift; sourceTree = "<group>"; };
EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenCoordinator.swift; sourceTree = "<group>"; };
EA880E78AF4BD24E45A7808C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = "<group>"; };
EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxyProtocol.swift; sourceTree = "<group>"; };
EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenSection.swift; sourceTree = "<group>"; };
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = "<group>"; };
EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenModels.swift; sourceTree = "<group>"; };
@@ -2650,6 +2663,7 @@
EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = "<group>"; };
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyMock.swift; sourceTree = "<group>"; };
EF98A02DED04075F7CF0C721 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = "<group>"; };
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = "<group>"; };
@@ -2951,6 +2965,7 @@
BDCEF7C3BF6D09F5611CFC8B /* SecureBackup */,
82D5AD3EAE3A5C1068A44A88 /* Session */,
5329E48968EB951235E83DAE /* SessionVerification */,
AAAB1791344F28CDC62E764D /* Spaces */,
AA94873C78BBB969CE65EE52 /* StateMachine */,
FCDF06BDB123505F0334B4F9 /* Timeline */,
E4E42F93A69AE52E6FAE9412 /* Users */,
@@ -3420,6 +3435,9 @@
FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */,
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */,
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */,
7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */,
AF771312D208C8002A2F05C4 /* SpaceRoomProxyMock.swift */,
EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */,
B0F5CC38803B8382D2C63222 /* TimelineControllerFactoryMock.swift */,
62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */,
17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */,
@@ -5138,7 +5156,9 @@
985411D514A5913EB1B60B54 /* Spaces */ = {
isa = PBXGroup;
children = (
BDDD421CD80AD0BCBA035076 /* Common */,
FCF165F4DDB83F3DECFEB57A /* SpaceListScreen */,
C360FCF7418FE3593D5A0CBF /* SpaceScreen */,
);
path = Spaces;
sourceTree = "<group>";
@@ -5382,6 +5402,16 @@
path = StateMachine;
sourceTree = "<group>";
};
AAAB1791344F28CDC62E764D /* Spaces */ = {
isa = PBXGroup;
children = (
EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */,
A01F2E9FD8FF95AB89A345E6 /* SpaceRoomProxyProtocol.swift */,
C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */,
);
path = Spaces;
sourceTree = "<group>";
};
AAFDD509929A0CCF8BCE51EB /* Authentication */ = {
isa = PBXGroup;
children = (
@@ -5543,6 +5573,14 @@
path = SecureBackup;
sourceTree = "<group>";
};
BDDD421CD80AD0BCBA035076 /* Common */ = {
isa = PBXGroup;
children = (
A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */,
);
path = Common;
sourceTree = "<group>";
};
BE7641A284D3E81DC96943E3 /* View */ = {
isa = PBXGroup;
children = (
@@ -5655,6 +5693,13 @@
path = SessionVerificationScreen;
sourceTree = "<group>";
};
C360FCF7418FE3593D5A0CBF /* SpaceScreen */ = {
isa = PBXGroup;
children = (
);
path = SpaceScreen;
sourceTree = "<group>";
};
C45CF12DD74BF5B6C970C5E1 /* RoomDirectorySearchScreen */ = {
isa = PBXGroup;
children = (
@@ -8031,6 +8076,13 @@
F38186A943D078D30BFB90DE /* SpaceListScreenModels.swift in Sources */,
F396470968764E2C3EDA92DA /* SpaceListScreenViewModel.swift in Sources */,
368EC173453FB805C677BFEF /* SpaceListScreenViewModelProtocol.swift in Sources */,
9B84F55288AB98783C11CC49 /* SpaceRoomCell.swift in Sources */,
9FBF0078657CA4F2A9747E98 /* SpaceRoomListProxyMock.swift in Sources */,
2EAA1B35D9CA24F090F48792 /* SpaceRoomListProxyProtocol.swift in Sources */,
C73CF79A578133D3AB7FB83D /* SpaceRoomProxyMock.swift in Sources */,
BE8075CA131C5EA3665C9E0D /* SpaceRoomProxyProtocol.swift in Sources */,
A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */,
DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */,
DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */,
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */,
3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */,
@@ -87,13 +87,16 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentSpaceList() {
let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession)
// Temporarily using the mock until the SDK is updated.
let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession, spaceServiceProxy: SpaceServiceProxyMock(.init()))
let coordinator = SpaceListScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .showSettings:
actionsSubject.send(.showSettings)
case .selectSpace:
break
}
}
.store(in: &cancellables)
@@ -15834,6 +15834,173 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
}
}
class SpaceRoomListProxyMock: SpaceRoomListProxyProtocol, @unchecked Sendable {
var spaceRoom: SpaceRoomProxyProtocol {
get { return underlyingSpaceRoom }
set(value) { underlyingSpaceRoom = value }
}
var underlyingSpaceRoom: SpaceRoomProxyProtocol!
var spaceRoomsPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> {
get { return underlyingSpaceRoomsPublisher }
set(value) { underlyingSpaceRoomsPublisher = value }
}
var underlyingSpaceRoomsPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never>!
var paginationStatePublisher: CurrentValuePublisher<SpaceRoomListProxyPaginationState, Never> {
get { return underlyingPaginationStatePublisher }
set(value) { underlyingPaginationStatePublisher = value }
}
var underlyingPaginationStatePublisher: CurrentValuePublisher<SpaceRoomListProxyPaginationState, Never>!
//MARK: - paginate
var paginateUnderlyingCallsCount = 0
var paginateCallsCount: Int {
get {
if Thread.isMainThread {
return paginateUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = paginateUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
paginateUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
paginateUnderlyingCallsCount = newValue
}
}
}
}
var paginateCalled: Bool {
return paginateCallsCount > 0
}
var paginateClosure: (() async -> Void)?
func paginate() async {
paginateCallsCount += 1
await paginateClosure?()
}
}
class SpaceRoomProxyMock: SpaceRoomProxyProtocol, @unchecked Sendable {
var id: String {
get { return underlyingId }
set(value) { underlyingId = value }
}
var underlyingId: String!
var name: String?
var avatarURL: URL?
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var childrenCount: Int {
get { return underlyingChildrenCount }
set(value) { underlyingChildrenCount = value }
}
var underlyingChildrenCount: Int!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var heroes: [UserProfileProxy] = []
var topic: String?
var canonicalAlias: String?
var joinRule: JoinRule?
var worldReadable: Bool?
var guestCanJoin: Bool {
get { return underlyingGuestCanJoin }
set(value) { underlyingGuestCanJoin = value }
}
var underlyingGuestCanJoin: Bool!
var state: Membership?
}
class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> {
get { return underlyingJoinedSpacesPublisher }
set(value) { underlyingJoinedSpacesPublisher = value }
}
var underlyingJoinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never>!
//MARK: - spaceRoomList
var spaceRoomListForUnderlyingCallsCount = 0
var spaceRoomListForCallsCount: Int {
get {
if Thread.isMainThread {
return spaceRoomListForUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = spaceRoomListForUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
spaceRoomListForUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
spaceRoomListForUnderlyingCallsCount = newValue
}
}
}
}
var spaceRoomListForCalled: Bool {
return spaceRoomListForCallsCount > 0
}
var spaceRoomListForReceivedSpaceRoom: SpaceRoomProxyProtocol?
var spaceRoomListForReceivedInvocations: [SpaceRoomProxyProtocol] = []
var spaceRoomListForUnderlyingReturnValue: Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>!
var spaceRoomListForReturnValue: Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>! {
get {
if Thread.isMainThread {
return spaceRoomListForUnderlyingReturnValue
} else {
var returnValue: Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>? = nil
DispatchQueue.main.sync {
returnValue = spaceRoomListForUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
spaceRoomListForUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
spaceRoomListForUnderlyingReturnValue = newValue
}
}
}
}
var spaceRoomListForClosure: ((SpaceRoomProxyProtocol) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>)?
func spaceRoomList(for spaceRoom: SpaceRoomProxyProtocol) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError> {
spaceRoomListForCallsCount += 1
spaceRoomListForReceivedSpaceRoom = spaceRoom
DispatchQueue.main.async {
self.spaceRoomListForReceivedInvocations.append(spaceRoom)
}
if let spaceRoomListForClosure = spaceRoomListForClosure {
return await spaceRoomListForClosure(spaceRoom)
} else {
return spaceRoomListForReturnValue
}
}
}
class StaticRoomSummaryProviderMock: StaticRoomSummaryProviderProtocol, @unchecked Sendable {
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
get { return underlyingRoomListPublisher }
@@ -0,0 +1,23 @@
//
// 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
import MatrixRustSDK
extension SpaceRoomListProxyMock {
struct Configuration {
var spaceRoomProxy: SpaceRoomProxyProtocol
var spaceRooms: [SpaceRoomProxyProtocol] = []
}
convenience init(_ configuration: Configuration) {
self.init()
spaceRoom = configuration.spaceRoomProxy
spaceRoomsPublisher = .init(configuration.spaceRooms)
}
}
@@ -0,0 +1,48 @@
//
// 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
import MatrixRustSDK
extension SpaceRoomProxyMock {
struct Configuration {
var id: String = UUID().uuidString
var name: String?
var avatarURL: URL?
var isSpace: Bool
var childrenCount = 0
var joinedMembersCount = 0
var heroes: [UserProfileProxy] = []
var topic: String?
var canonicalAlias: String?
var joinRule: JoinRule?
var worldReadable: Bool?
var guestCanJoin = true
var state: Membership?
}
convenience init(_ configuration: Configuration) {
self.init()
id = configuration.id
name = configuration.name
avatarURL = configuration.avatarURL
isSpace = configuration.isSpace
childrenCount = configuration.childrenCount
joinedMembersCount = configuration.joinedMembersCount
heroes = configuration.heroes
topic = configuration.topic
canonicalAlias = configuration.canonicalAlias
joinRule = configuration.joinRule
worldReadable = configuration.worldReadable
guestCanJoin = configuration.guestCanJoin
state = configuration.state
}
}
@@ -0,0 +1,29 @@
//
// 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
import MatrixRustSDK
extension SpaceServiceProxyMock {
struct Configuration {
var joinedSpaces: [SpaceRoomProxyProtocol] = []
var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:]
}
convenience init(_ configuration: Configuration) {
self.init()
joinedSpacesPublisher = .init(configuration.joinedSpaces)
spaceRoomListForClosure = { spaceRoom in
if let spaceRoomList = configuration.spaceRoomLists[spaceRoom.id] {
.success(spaceRoomList)
} else {
.failure(.sdkError(ClientProxyMockError.generic))
}
}
}
}
@@ -42,6 +42,7 @@ enum A11yIdentifiers {
static let pollFormScreen = PollFormScreen()
static let roomPollsHistoryScreen = RoomPollsHistoryScreen()
static let manageRoomMemberSheet = ManageRoomMemberSheet()
static let spaceListScreen = SpaceListScreen()
struct AlertInfo {
let primaryButton = "alert_info-primary_button"
@@ -297,4 +298,13 @@ enum A11yIdentifiers {
struct ManageRoomMemberSheet {
let viewProfile = "manage_room_member_sheet-view_profile"
}
struct SpaceListScreen {
let userAvatar = "space_list_screen-user_avatar"
let roomNamePrefix = "space_list_screen-room_name"
func spaceRoomName(_ name: String) -> String {
"\(roomNamePrefix):\(name)"
}
}
}
+15 -10
View File
@@ -59,8 +59,9 @@ enum Avatars {
}
enum UserAvatarSizeOnScreen {
case chats
case spaces
case timeline
case home
case settings
case roomDetails
case dmDetails
@@ -82,13 +83,13 @@ enum UserAvatarSizeOnScreen {
var value: CGFloat {
switch self {
case .readReceipt:
return 16
case .readReceiptSheet:
case .chats, .spaces:
return 32
case .timeline:
return 32
case .home:
case .readReceipt:
return 16
case .readReceiptSheet:
return 32
case .completionSuggestions:
return 32
@@ -127,8 +128,9 @@ enum UserAvatarSizeOnScreen {
}
enum RoomAvatarSizeOnScreen {
case chats
case spaces
case timeline
case home
case messageForwarding
case globalSearch
case roomSelection
@@ -136,14 +138,17 @@ enum RoomAvatarSizeOnScreen {
case notificationSettings
case roomDirectorySearch
case joinRoom
case spaceHeader
case completionSuggestions
var value: CGFloat {
switch self {
case .notificationSettings:
return 30
case .chats, .spaces:
return 52
case .timeline:
return 32
case .notificationSettings:
return 30
case .roomDirectorySearch:
return 32
case .completionSuggestions:
@@ -154,12 +159,12 @@ enum RoomAvatarSizeOnScreen {
return 36
case .roomSelection:
return 36
case .home:
return 52
case .details:
return 96
case .joinRoom:
return 96
case .spaceHeader:
return 64
}
}
}
@@ -132,46 +132,46 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview {
RoomAvatarImage(avatar: .room(id: "!1:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .room(id: "!2:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!space:server.com",
name: "Room",
avatarURL: nil),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .space(id: "!otherspace:server.com",
name: "Room",
avatarURL: .mockMXCAvatar),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .tombstoned, avatarSize: .room(on: .chats), mediaProvider: MediaProviderMock(configuration: .init()))
}
HStack(spacing: 12) {
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: nil)]),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com",
displayName: "User",
avatarURL: .mockMXCAvatar)]),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil),
.init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]),
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: MediaProviderMock(configuration: .init()))
}
}
@@ -37,7 +37,7 @@ struct TombstonedAvatarImage: View {
struct TombstonedAvatarImage_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
TombstonedAvatarImage(avatarSize: .room(on: .home))
TombstonedAvatarImage(avatarSize: .room(on: .chats))
.previewLayout(.sizeThatFits)
}
}
@@ -154,6 +154,7 @@ enum TestablePreviewsDictionary {
"ShimmerOverlay_Previews" : ShimmerOverlay_Previews.self,
"SoftLogoutScreen_Previews" : SoftLogoutScreen_Previews.self,
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
"SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self,
"SplashScreen_Previews" : SplashScreen_Previews.self,
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
"StartChatScreen_Previews" : StartChatScreen_Previews.self,
@@ -40,7 +40,7 @@ struct HomeScreen: View {
LoadableAvatarImage(url: context.viewState.userAvatarURL,
name: context.viewState.userDisplayName,
contentID: context.viewState.userID,
avatarSize: .user(on: .home),
avatarSize: .user(on: .chats),
mediaProvider: context.mediaProvider)
.accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar)
.overlayBadge(10, isBadged: context.viewState.requiresExtraAccountSetup)
@@ -50,7 +50,7 @@ struct HomeScreenRoomCell: View {
private var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
avatarSize: .room(on: .home),
avatarSize: .room(on: .chats),
mediaProvider: context.mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
@@ -0,0 +1,179 @@
//
// 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 Compound
import SwiftUI
struct SpaceRoomCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let spaceRoom: SpaceRoomProxyProtocol
let isSelected: Bool
let mediaProvider: MediaProviderProtocol!
enum Action { case select(SpaceRoomProxyProtocol), join(SpaceRoomProxyProtocol) }
let action: (Action) -> Void
private let verticalInsets = 12.0
private let horizontalInsets = 16.0
private var subtitle: String {
if spaceRoom.isSpace {
spaceRoom.joinRule == .public ? L10n.commonPublicSpace : L10n.commonPrivateSpace
} else {
L10n.commonMemberCount(spaceRoom.joinedMembersCount)
}
}
private var details: String {
if spaceRoom.isSpace {
L10n.screenSpaceListDetails(L10n.commonRooms(spaceRoom.childrenCount),
L10n.commonMemberCount(spaceRoom.joinedMembersCount))
} else {
spaceRoom.topic ?? " " // Use a single space to reserve a consistent amount of space.
}
}
var body: some View {
Button {
action(.select(spaceRoom))
} label: {
HStack(spacing: 16.0) {
avatar
content
.padding(.vertical, verticalInsets)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
.padding(.trailing, -horizontalInsets)
}
}
.padding(.horizontal, horizontalInsets)
.accessibilityElement(children: .combine)
}
.buttonStyle(SpaceRoomCellButtonStyle(isSelected: isSelected))
.accessibilityIdentifier(A11yIdentifiers.spaceListScreen.spaceRoomName(spaceRoom.name ?? spaceRoom.id))
}
@ViewBuilder @MainActor
private var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: spaceRoom.avatar,
avatarSize: .room(on: .spaces),
mediaProvider: mediaProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
}
private var content: some View {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 2) {
Text(spaceRoom.name ?? spaceRoom.id)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
visibilityLabel
Text(details)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
accessory
}
}
private var visibilityLabel: some View {
Label {
Text(subtitle)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.lineLimit(1)
} icon: {
CompoundIcon(spaceRoom.joinRule == .public ? \.public : \.lockSolid,
size: .xSmall,
relativeTo: .compound.bodyMD)
.foregroundStyle(.compound.iconTertiary)
}
.labelStyle(.custom(spacing: 4))
}
@ViewBuilder
private var accessory: some View {
switch spaceRoom.state {
case .none, .left, .invited:
Button(L10n.actionJoin) { action(.join(spaceRoom)) }
.font(.compound.bodyLG)
.foregroundStyle(.compound.textActionAccent)
case .joined, .knocked, .banned:
EmptyView()
}
}
}
struct SpaceRoomCellButtonStyle: ButtonStyle {
let isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(isSelected || configuration.isPressed ? Color.compound.bgSubtleSecondary : Color.compound.bgCanvasDefault)
.contentShape(Rectangle())
.animation(isSelected ? .none : .easeOut(duration: 0.1).disabledDuringTests(), value: isSelected)
}
}
struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = makeSpaceRooms(isSpace: true)
static let rooms = makeSpaceRooms(isSpace: false)
static var previews: some View {
VStack(spacing: 0) {
ForEach(spaces, id: \.id) { space in
SpaceRoomCell(spaceRoom: space,
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))
]
}
}
@@ -12,9 +12,11 @@ import SwiftUI
struct SpaceListScreenCoordinatorParameters {
let userSession: UserSessionProtocol
let spaceServiceProxy: SpaceServiceProxyProtocol
}
enum SpaceListScreenCoordinatorAction {
case selectSpace(SpaceRoomProxyProtocol)
case showSettings
}
@@ -32,7 +34,8 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol {
init(parameters: SpaceListScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = SpaceListScreenViewModel(userSession: parameters.userSession)
viewModel = SpaceListScreenViewModel(userSession: parameters.userSession,
spaceServiceProxy: parameters.spaceServiceProxy)
}
func start() {
@@ -41,6 +44,8 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .selectSpace(let spaceRoom):
actionsSubject.send(.selectSpace(spaceRoom))
case .showSettings:
actionsSubject.send(.showSettings)
}
@@ -8,6 +8,7 @@
import Foundation
enum SpaceListScreenViewModelAction {
case selectSpace(SpaceRoomProxyProtocol)
case showSettings
}
@@ -16,18 +17,19 @@ struct SpaceListScreenViewState: BindableState {
var userDisplayName: String?
var userAvatarURL: URL?
var rooms: [HomeScreenRoom]
var joinedSpaces: [SpaceRoomProxyProtocol]
var joinedRoomsCount: Int
var bindings: SpaceListScreenViewStateBindings
var subtitle: String {
L10n.screenSpaceListDetails(L10n.commonSpaces(rooms.count), L10n.commonRooms(joinedRoomsCount))
L10n.screenSpaceListDetails(L10n.commonSpaces(joinedSpaces.count), L10n.commonRooms(joinedRoomsCount))
}
}
struct SpaceListScreenViewStateBindings { }
enum SpaceListScreenViewAction {
case spaceAction(SpaceRoomCell.Action)
case showSettings
}
@@ -11,18 +11,26 @@ import SwiftUI
typealias SpaceListScreenViewModelType = StateStoreViewModelV2<SpaceListScreenViewState, SpaceListScreenViewAction>
class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenViewModelProtocol {
private let spaceServiceProxy: SpaceServiceProxyProtocol
private let actionsSubject: PassthroughSubject<SpaceListScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<SpaceListScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(userSession: UserSessionProtocol) {
init(userSession: UserSessionProtocol, spaceServiceProxy: SpaceServiceProxyProtocol) {
self.spaceServiceProxy = spaceServiceProxy
super.init(initialViewState: SpaceListScreenViewState(userID: userSession.clientProxy.userID,
rooms: [],
joinedSpaces: spaceServiceProxy.joinedSpacesPublisher.value,
joinedRoomsCount: 0,
bindings: .init()),
mediaProvider: userSession.mediaProvider)
spaceServiceProxy.joinedSpacesPublisher
.weakAssign(to: \.state.joinedSpaces, on: self)
.store(in: &cancellables)
userSession.clientProxy.userAvatarURLPublisher
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.userAvatarURL, on: self)
@@ -40,6 +48,10 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .spaceAction(.select(let spaceRoom)):
actionsSubject.send(.selectSpace(spaceRoom))
case .spaceAction(.join(let spaceRoom)):
#warning("Implement joining.")
case .showSettings:
actionsSubject.send(.showSettings)
}
@@ -15,6 +15,7 @@ struct SpaceListScreen: View {
ScrollView {
LazyVStack(spacing: 0) {
header
spaces
}
}
.navigationTitle(L10n.screenSpaceListTitle)
@@ -56,6 +57,16 @@ struct SpaceListScreen: View {
}
}
var spaces: some View {
ForEach(context.viewState.joinedSpaces, id: \.id) { spaceRoom in
SpaceRoomCell(spaceRoom: spaceRoom,
isSelected: false,
mediaProvider: context.mediaProvider) { action in
context.send(viewAction: .spaceAction(action))
}
}
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
@@ -65,7 +76,7 @@ struct SpaceListScreen: View {
LoadableAvatarImage(url: context.viewState.userAvatarURL,
name: context.viewState.userDisplayName,
contentID: context.viewState.userID,
avatarSize: .user(on: .home),
avatarSize: .user(on: .spaces),
mediaProvider: context.mediaProvider)
.accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar)
.compositingGroup()
@@ -94,7 +105,56 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview {
static func makeViewModel(counterValue: Int = 0) -> SpaceListScreenViewModel {
let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let viewModel = SpaceListScreenViewModel(userSession: userSession)
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)
return viewModel
}
@@ -0,0 +1,25 @@
//
// 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 MatrixRustSDK
// Temporary until the SDK is updated.
enum SpaceRoomListProxyPaginationState {
case idle(endReached: Bool)
case loading
}
// sourcery: AutoMockable
protocol SpaceRoomListProxyProtocol {
var spaceRoom: SpaceRoomProxyProtocol { get }
var spaceRoomsPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get }
var paginationStatePublisher: CurrentValuePublisher<SpaceRoomListProxyPaginationState, Never> { get }
func paginate() async
}
@@ -0,0 +1,39 @@
//
// 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
import MatrixRustSDK
// sourcery: AutoMockable
protocol SpaceRoomProxyProtocol {
var id: String { get }
var name: String? { get }
var avatarURL: URL? { get }
var isSpace: Bool { get }
var childrenCount: Int { get }
var joinedMembersCount: Int { get }
var heroes: [UserProfileProxy] { get }
var topic: String? { get }
var canonicalAlias: String? { get }
var joinRule: JoinRule? { get }
var worldReadable: Bool? { get }
var guestCanJoin: Bool { get }
var state: Membership? { get }
}
extension SpaceRoomProxyProtocol {
var avatar: RoomAvatar {
if isSpace {
.space(id: id, name: name, avatarURL: avatarURL)
} else { // We don't need to check for heroes, we only do that for DMs.
.room(id: id, name: name, avatarURL: avatarURL)
}
}
}
@@ -0,0 +1,19 @@
//
// 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 SpaceServiceProxyError: Error {
case sdkError(Error)
}
// sourcery: AutoMockable
protocol SpaceServiceProxyProtocol {
var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get }
func spaceRoomList(for spaceRoom: SpaceRoomProxyProtocol) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>
}
@@ -881,6 +881,12 @@ extension PreviewTests {
}
}
func testSpaceRoomCell() async throws {
for (index, preview) in SpaceRoomCell_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)
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60e71badaa8586cc67ae28fa888bf072e61bf8c547c708a8ec32461e33c86619
size 100453
oid sha256:3c569e1d6d8af58bfe5e80ef069c4003bec9cbf9d5ed392c1f4004a678f847d2
size 192352
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77dfe5e4cc52f77c46891842a27fdd99cdf510c303009d87b4285cd575938309
size 108725
oid sha256:1010ea05073ce4eb55fc791a44de8c0d03a3bfce63c706e809b22ed00526dad3
size 235707
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65416f910dc4c02a093fe7accc67117a32cc12d8a6a1bf26977e4493d8bb0d19
size 55676
oid sha256:a096fa0a28ac3032ce18ff7ab44278abefb6634a285df81125c268f6ee04ed88
size 136997
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6f1b42f4cd90cbedcc744b7c5b9794b7b84b51d0ad4d083bb5837d7a9afc142
size 70168
oid sha256:0a14a67f73ca88566f80e6c4c086f1f56ac52a8d4bc31a522a2e09371711c8f4
size 162833
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04df1d04edeac89a7321c3e02ed38ee85bfa0946337896a334d89d8c561195ff
size 207862
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9eda888ec16f20eb1f116ce198ef518cdeb2795c8c59b674c0d537cca2acdc97
size 237007
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2cf45ff8958dfa9af8bd456e0d72c94c70d769461394ec028dcbe120ba714957
size 149449
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf1fd488f8022708dcedfd1f367b3cf688e9153e5505c1657810d6f4959b76a0
size 167283
@@ -5,12 +5,16 @@
// Please see LICENSE files in the repository root for full details.
//
import Combine
import XCTest
@testable import ElementX
@MainActor
class SpaceListScreenViewModelTests: XCTestCase {
var joinedSpacesSubject: CurrentValueSubject<[SpaceRoomProxyProtocol], Never>!
var spaceServiceProxy: SpaceServiceProxyMock!
var viewModel: SpaceListScreenViewModelProtocol!
var context: SpaceListScreenViewModelType.Context {
@@ -19,15 +23,52 @@ class SpaceListScreenViewModelTests: XCTestCase {
func testInitialState() {
setupViewModel()
XCTAssertTrue(context.viewState.rooms.isEmpty)
XCTAssertEqual(context.viewState.joinedSpaces.count, 3)
XCTAssertEqual(context.viewState.joinedRoomsCount, 0)
}
func testJoinedSpacesSubscription() {
setupViewModel()
joinedSpacesSubject.send([])
XCTAssertEqual(context.viewState.joinedSpaces.count, 0)
joinedSpacesSubject.send([
SpaceRoomProxyMock(.init(isSpace: true))
])
XCTAssertEqual(context.viewState.joinedSpaces.count, 1)
}
func testSelectingSpace() async throws {
setupViewModel()
let selectedSpace = joinedSpacesSubject.value[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() {
let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
viewModel = SpaceListScreenViewModel(userSession: userSession)
joinedSpacesSubject = .init([
SpaceRoomProxyMock(.init(id: "space1", isSpace: true)),
SpaceRoomProxyMock(.init(id: "space2", isSpace: true)),
SpaceRoomProxyMock(.init(id: "space3", isSpace: true))
])
spaceServiceProxy = SpaceServiceProxyMock(.init())
spaceServiceProxy.joinedSpacesPublisher = joinedSpacesSubject.asCurrentValuePublisher()
viewModel = SpaceListScreenViewModel(userSession: userSession, spaceServiceProxy: spaceServiceProxy)
}
}