diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 43c5323e8..096579847 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -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") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 72dc41db8..13a9caf4c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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 = ""; }; 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = ""; }; 7D39AF1F659923D77778511E /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = ""; }; + 7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxyMock.swift; sourceTree = ""; }; 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModel.swift; sourceTree = ""; }; 7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2246,9 +2254,11 @@ A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = ""; }; A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = ""; }; + A01F2E9FD8FF95AB89A345E6 /* SpaceRoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomProxyProtocol.swift; sourceTree = ""; }; A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = ""; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetFlowCoordinator.swift; sourceTree = ""; }; + A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomCell.swift; sourceTree = ""; }; A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenKnockedCell.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2327,6 +2337,7 @@ AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; AF2E6ADAE685F4109B1FE795 /* TimelineThreadSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineThreadSummaryView.swift; sourceTree = ""; }; + AF771312D208C8002A2F05C4 /* SpaceRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomProxyMock.swift; sourceTree = ""; }; AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = ""; }; @@ -2442,6 +2453,7 @@ C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderStateTests.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; + C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyProtocol.swift; sourceTree = ""; }; C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = ""; }; C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = ""; }; C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2626,6 +2638,7 @@ EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModelTests.swift; sourceTree = ""; }; EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenCoordinator.swift; sourceTree = ""; }; EA880E78AF4BD24E45A7808C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; + EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxyProtocol.swift; sourceTree = ""; }; EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenSection.swift; sourceTree = ""; }; EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = ""; }; EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenModels.swift; sourceTree = ""; }; @@ -2650,6 +2663,7 @@ EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = ""; }; EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = ""; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = ""; }; + EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyMock.swift; sourceTree = ""; }; EF98A02DED04075F7CF0C721 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = ""; }; EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = ""; }; @@ -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 = ""; @@ -5382,6 +5402,16 @@ path = StateMachine; sourceTree = ""; }; + AAAB1791344F28CDC62E764D /* Spaces */ = { + isa = PBXGroup; + children = ( + EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */, + A01F2E9FD8FF95AB89A345E6 /* SpaceRoomProxyProtocol.swift */, + C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */, + ); + path = Spaces; + sourceTree = ""; + }; AAFDD509929A0CCF8BCE51EB /* Authentication */ = { isa = PBXGroup; children = ( @@ -5543,6 +5573,14 @@ path = SecureBackup; sourceTree = ""; }; + BDDD421CD80AD0BCBA035076 /* Common */ = { + isa = PBXGroup; + children = ( + A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */, + ); + path = Common; + sourceTree = ""; + }; BE7641A284D3E81DC96943E3 /* View */ = { isa = PBXGroup; children = ( @@ -5655,6 +5693,13 @@ path = SessionVerificationScreen; sourceTree = ""; }; + C360FCF7418FE3593D5A0CBF /* SpaceScreen */ = { + isa = PBXGroup; + children = ( + ); + path = SpaceScreen; + sourceTree = ""; + }; 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 */, diff --git a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift index 17916bcf5..820eaec7e 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift @@ -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) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 7f296995a..fd034d47d 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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 { + get { return underlyingPaginationStatePublisher } + set(value) { underlyingPaginationStatePublisher = value } + } + var underlyingPaginationStatePublisher: CurrentValuePublisher! + + //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! + var spaceRoomListForReturnValue: Result! { + get { + if Thread.isMainThread { + return spaceRoomListForUnderlyingReturnValue + } else { + var returnValue: Result? = 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)? + + func spaceRoomList(for spaceRoom: SpaceRoomProxyProtocol) async -> Result { + 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 } diff --git a/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift b/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift new file mode 100644 index 000000000..662d603f8 --- /dev/null +++ b/ElementX/Sources/Mocks/SpaceRoomListProxyMock.swift @@ -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) + } +} diff --git a/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift new file mode 100644 index 000000000..b659e6cc6 --- /dev/null +++ b/ElementX/Sources/Mocks/SpaceRoomProxyMock.swift @@ -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 + } +} diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift new file mode 100644 index 000000000..93dd661f6 --- /dev/null +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -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)) + } + } + } +} diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 8cedb2c5b..a8d309f82 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -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)" + } + } } diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 98027076b..e81e2688d 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -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 } } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift index 2bcc8dcb9..0e865eb66 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift @@ -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())) } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/TombstonedAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/TombstonedAvatarImage.swift index 0a929dbce..487998a61 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/TombstonedAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/TombstonedAvatarImage.swift @@ -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) } } diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 5d2c40cb4..d4d1aa699 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -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, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index cbc4a24eb..c1bf6e4d5 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -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) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index b0b85cbef..d7cdebee3 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -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) diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift new file mode 100644 index 000000000..77a8bc75b --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift @@ -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)) + ] + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift index 2ffa6b908..be1096ebd 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift @@ -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) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift index b8c766860..9fec3ba83 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift @@ -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 } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift index dc423da05..a120c4edf 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift @@ -11,18 +11,26 @@ import SwiftUI typealias SpaceListScreenViewModelType = StateStoreViewModelV2 class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenViewModelProtocol { + private let spaceServiceProxy: SpaceServiceProxyProtocol + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { 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) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift index da3a5481f..517aedf0b 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift @@ -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 } diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift new file mode 100644 index 000000000..ccdd06516 --- /dev/null +++ b/ElementX/Sources/Services/Spaces/SpaceRoomListProxyProtocol.swift @@ -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 { get } + + func paginate() async +} diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceRoomProxyProtocol.swift new file mode 100644 index 000000000..6086723e9 --- /dev/null +++ b/ElementX/Sources/Services/Spaces/SpaceRoomProxyProtocol.swift @@ -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) + } + } +} diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift new file mode 100644 index 000000000..8e6403600 --- /dev/null +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift @@ -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 +} diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 14035e40a..48ff62a53 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -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) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png index 27da36f78..5da5c061b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60e71badaa8586cc67ae28fa888bf072e61bf8c547c708a8ec32461e33c86619 -size 100453 +oid sha256:3c569e1d6d8af58bfe5e80ef069c4003bec9cbf9d5ed392c1f4004a678f847d2 +size 192352 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-pseudo-0.png index db0128a35..a76d23117 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77dfe5e4cc52f77c46891842a27fdd99cdf510c303009d87b4285cd575938309 -size 108725 +oid sha256:1010ea05073ce4eb55fc791a44de8c0d03a3bfce63c706e809b22ed00526dad3 +size 235707 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png index 82155dc63..6d97e2584 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65416f910dc4c02a093fe7accc67117a32cc12d8a6a1bf26977e4493d8bb0d19 -size 55676 +oid sha256:a096fa0a28ac3032ce18ff7ab44278abefb6634a285df81125c268f6ee04ed88 +size 136997 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-pseudo-0.png index 6d332bfb6..aeb826ce3 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceListScreen.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6f1b42f4cd90cbedcc744b7c5b9794b7b84b51d0ad4d083bb5837d7a9afc142 -size 70168 +oid sha256:0a14a67f73ca88566f80e6c4c086f1f56ac52a8d4bc31a522a2e09371711c8f4 +size 162833 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png new file mode 100644 index 000000000..91df5863a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04df1d04edeac89a7321c3e02ed38ee85bfa0946337896a334d89d8c561195ff +size 207862 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png new file mode 100644 index 000000000..5a40b655e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eda888ec16f20eb1f116ce198ef518cdeb2795c8c59b674c0d537cca2acdc97 +size 237007 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..74fc929ba --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cf45ff8958dfa9af8bd456e0d72c94c70d769461394ec028dcbe120ba714957 +size 149449 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..ff29f11a5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spaceRoomCell.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf1fd488f8022708dcedfd1f367b3cf688e9153e5505c1657810d6f4959b76a0 +size 167283 diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index aec15b14a..580284a25 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -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) } }