diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 6f4987e69..e73d83721 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -75,6 +75,14 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "CallNotificationRoomTimelineView_Previews") } + func testChatsSpaceFilterCell() async throws { + try await performAccessibilityAudit(named: "ChatsSpaceFilterCell_Previews") + } + + func testChatsSpaceFiltersScreen() async throws { + try await performAccessibilityAudit(named: "ChatsSpaceFiltersScreen_Previews") + } + func testCollapsibleRoomTimelineView() async throws { try await performAccessibilityAudit(named: "CollapsibleRoomTimelineView_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 233bc6646..bc1a7ed5c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -355,6 +355,7 @@ 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */; }; 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */; }; + 3E2EE5782688054B3026EF8E /* ChatsSpaceFilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C893CF354BCBDF1351C13 /* ChatsSpaceFilterCell.swift */; }; 3E3CC3D17908A3BB9F224CC5 /* DeactivateAccountScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67638D34FB6A9110BE481A6F /* DeactivateAccountScreenCoordinator.swift */; }; 3E7B65C2C97748D5D65AAA8B /* NotificationPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */; }; 3EC5A41F9FB7DD63A4DC6144 /* RoomChangeRolesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14B1DE3E2D5D26732C49036 /* RoomChangeRolesScreenViewModel.swift */; }; @@ -800,6 +801,7 @@ 89DF67AECBF9D0EE0DDB7737 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; }; 89E6426C6097F848C125E65C /* SpacesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD297970D7A0F8BAF870F010 /* SpacesScreenModels.swift */; }; 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */; }; + 8A2B2836B1B0CCA3CC140560 /* ChatsSpaceFiltersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859F51637DA710BBE7B70D6D /* ChatsSpaceFiltersScreenViewModel.swift */; }; 8A5064CAC8E5F3B18645621D /* CallNotificationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */; }; 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; }; 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; }; @@ -929,6 +931,7 @@ A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; }; A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; }; A07178337F3C0B208B5A77A8 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6ED50FE104992419310EEB /* NotificationHandler.swift */; }; + A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */; }; A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */; }; A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */; }; A0D7E5BD0298A97DCBDCE40B /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; }; @@ -1216,6 +1219,7 @@ D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; + D4F3C5656DF09037825E9965 /* ChatsSpaceFiltersScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF2C15634499348A512A93A /* ChatsSpaceFiltersScreenModels.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; @@ -1304,6 +1308,7 @@ E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; E6A2F6E4795C1054025AACFF /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AB32690C6CE5C62A86D6FA /* NotificationItemProxy.swift */; }; E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */; }; + E713F930C5AD0F4B11119E2B /* ChatsSpaceFiltersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 154664B1B0D71846FBAC6E61 /* ChatsSpaceFiltersScreen.swift */; }; E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; }; E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */; }; E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; }; @@ -1668,6 +1673,7 @@ 14517E5597594956FCE1950D /* RoomInfoProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInfoProxyProtocol.swift; sourceTree = ""; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = ""; }; 1511B1DCECC0DC75EB267328 /* KnockRequestsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreen.swift; sourceTree = ""; }; + 154664B1B0D71846FBAC6E61 /* ChatsSpaceFiltersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsSpaceFiltersScreen.swift; sourceTree = ""; }; 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenCoordinator.swift; sourceTree = ""; }; 15748C254911E3654C93B0ED /* MentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionBuilder.swift; sourceTree = ""; }; 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorProtocol.swift; sourceTree = ""; }; @@ -2261,6 +2267,7 @@ 840182D7A61402D5947DE094 /* NotificationItemProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyMock.swift; sourceTree = ""; }; 84311D707B09854D67F78BBF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersViewModelTests.swift; sourceTree = ""; }; + 847C893CF354BCBDF1351C13 /* ChatsSpaceFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsSpaceFilterCell.swift; sourceTree = ""; }; 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelTests.swift; sourceTree = ""; }; 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreen.swift; sourceTree = ""; }; 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = ""; }; @@ -2275,6 +2282,7 @@ 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = ""; }; 8585C636A10B8141A7AE909F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockedRoomProxy.swift; sourceTree = ""; }; + 859F51637DA710BBE7B70D6D /* ChatsSpaceFiltersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsSpaceFiltersScreenViewModel.swift; sourceTree = ""; }; 85A1941B874A3BE9CDDF43EF /* XCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2497,6 +2505,7 @@ AE739A6836E86E3780748477 /* TimelineItemBubbleBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbleBackground.swift; sourceTree = ""; }; AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFNumberedListView.swift; sourceTree = ""; }; AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; + AEF2C15634499348A512A93A /* ChatsSpaceFiltersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsSpaceFiltersScreenModels.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 = ""; }; AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; @@ -2792,6 +2801,7 @@ E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = ""; }; E604A74AEFE0BF6151061291 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; E60757AFE04391B43EA568B8 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; + E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsSpaceFiltersScreenViewModelProtocol.swift; sourceTree = ""; }; E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersView.swift; sourceTree = ""; }; E66763BD54A3A1D9C6E6F2F1 /* PinnedItemsIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedItemsIndicatorView.swift; sourceTree = ""; }; E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenCoordinator.swift; sourceTree = ""; }; @@ -3724,6 +3734,17 @@ path = Views; sourceTree = ""; }; + 330B6E4BB92EA4AE6A95828E /* ChatsSpaceFiltersScreen */ = { + isa = PBXGroup; + children = ( + AEF2C15634499348A512A93A /* ChatsSpaceFiltersScreenModels.swift */, + 859F51637DA710BBE7B70D6D /* ChatsSpaceFiltersScreenViewModel.swift */, + E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */, + 82F77DDA38D41A185D3536F4 /* View */, + ); + path = ChatsSpaceFiltersScreen; + sourceTree = ""; + }; 3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */ = { isa = PBXGroup; children = ( @@ -5137,6 +5158,15 @@ path = Session; sourceTree = ""; }; + 82F77DDA38D41A185D3536F4 /* View */ = { + isa = PBXGroup; + children = ( + 847C893CF354BCBDF1351C13 /* ChatsSpaceFilterCell.swift */, + 154664B1B0D71846FBAC6E61 /* ChatsSpaceFiltersScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 832EB453B3A5D04C18D86117 /* View */ = { isa = PBXGroup; children = ( @@ -6427,6 +6457,7 @@ EFD4F7FCAAAB3EF45EE7A067 /* BlockedUsersScreen */, 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, 1185EECDD07495D65AC84AFC /* CallScreen */, + 330B6E4BB92EA4AE6A95828E /* ChatsSpaceFiltersScreen */, 90DC2E28718955ED87AD1456 /* CreatePollScreen */, 821EB0D1C0019E3C7BBAEDBB /* CreateRoomScreen */, 3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */, @@ -7934,6 +7965,11 @@ E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */, DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */, D885B783B95AD7832D4EF5DD /* CharacterSet.swift in Sources */, + 3E2EE5782688054B3026EF8E /* ChatsSpaceFilterCell.swift in Sources */, + E713F930C5AD0F4B11119E2B /* ChatsSpaceFiltersScreen.swift in Sources */, + D4F3C5656DF09037825E9965 /* ChatsSpaceFiltersScreenModels.swift in Sources */, + 8A2B2836B1B0CCA3CC140560 /* ChatsSpaceFiltersScreenViewModel.swift in Sources */, + A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */, 572474C7CA4B03FF0B5DF548 /* ChatsTabFlowCoordinator.swift in Sources */, BC5F94B10B40ABEC6046B473 /* ChatsTabFlowCoordinatorStateMachine.swift in Sources */, A52090A4FE0DB826578DFC03 /* Client.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index de8d23121..4d1fe2850 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -541,10 +541,14 @@ "screen_create_room_public_option_short_description" = "Anyone can join."; "screen_create_room_room_access_section_knocking_option_description" = "Anyone can ask to join but an administrator or a moderator must accept the request."; "screen_create_room_room_access_section_knocking_option_title" = "Allow ask to join"; +"screen_create_room_room_access_section_knocking_restricted_option_description" = "Anyone in %1$@ can join but everyone else must request access."; +"screen_create_room_room_access_section_knocking_restricted_option_title" = "Ask to join"; "screen_create_room_room_access_section_private_option_description" = "Only people invited can join."; "screen_create_room_room_access_section_private_option_title" = "Private"; "screen_create_room_room_access_section_public_option_description" = "Anyone can join."; "screen_create_room_room_access_section_public_option_title" = "Public"; +"screen_create_room_room_access_section_restricted_option_description" = "Anyone in %1$@ can join."; +"screen_create_room_room_access_section_restricted_option_title" = "Standard"; "screen_create_room_room_access_section_title" = "Who has access"; "screen_create_room_room_address_section_footer" = "You’ll need an address in order to make it visible in the public directory."; "screen_create_room_room_address_section_title" = "Address"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 78ef1010f..1018e8e54 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -541,10 +541,14 @@ "screen_create_room_public_option_short_description" = "Anyone can join."; "screen_create_room_room_access_section_knocking_option_description" = "Anyone can ask to join but an administrator or a moderator must accept the request."; "screen_create_room_room_access_section_knocking_option_title" = "Allow ask to join"; +"screen_create_room_room_access_section_knocking_restricted_option_description" = "Anyone in %1$@ can join but everyone else must request access."; +"screen_create_room_room_access_section_knocking_restricted_option_title" = "Ask to join"; "screen_create_room_room_access_section_private_option_description" = "Only people invited can join."; "screen_create_room_room_access_section_private_option_title" = "Private"; "screen_create_room_room_access_section_public_option_description" = "Anyone can join."; "screen_create_room_room_access_section_public_option_title" = "Public"; +"screen_create_room_room_access_section_restricted_option_description" = "Anyone in %1$@ can join."; +"screen_create_room_room_access_section_restricted_option_title" = "Standard"; "screen_create_room_room_access_section_title" = "Who has access"; "screen_create_room_room_address_section_footer" = "You’ll need an address in order to make it visible in the public directory."; "screen_create_room_room_address_section_title" = "Address"; diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 1a9017827..0125b2f48 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -67,6 +67,7 @@ final class AppSettings { case linkPreviewsEnabled case focusEventOnNotificationTap case linkNewDeviceEnabled + case spaceFiltersEnabled // Spaces case spaceSettingsEnabled @@ -420,6 +421,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.linkNewDeviceEnabled, defaultValue: false, storageType: .userDefaults(store)) var linkNewDeviceEnabled + @UserPreference(key: UserDefaultsKeys.spaceFiltersEnabled, defaultValue: false, storageType: .userDefaults(store)) + var spaceFiltersEnabled + @UserPreference(key: UserDefaultsKeys.developerOptionsEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) var developerOptionsEnabled } diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift index 9e2422ae9..9209aa1a9 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift @@ -684,7 +684,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { let hostingController = UIHostingController(rootView: coordinator.toPresentable()) hostingController.view.backgroundColor = .clear flowParameters.windowManager.globalSearchWindow.rootViewController = hostingController - + flowParameters.windowManager.showGlobalSearch() } diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift index f0aa33364..9e4b56d26 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift @@ -23,7 +23,7 @@ class ChatsTabFlowCoordinatorStateMachine { /// Showing the home screen. The `roomListSelectedRoomID` represents the timeline shown on the detail panel (if any) case roomList(detailState: DetailState?) - + /// Showing the feedback screen. case feedbackScreen(detailState: DetailState?) @@ -52,7 +52,7 @@ class ChatsTabFlowCoordinatorStateMachine { case declineAndBlockUserScreen(detailState: DetailState?) - /// The selected room ID from the state if available. + /// The state of the currently selected room var detailState: DetailState? { switch self { case .initial, .userProfileScreen, .shareExtensionRoomList: @@ -157,7 +157,7 @@ class ChatsTabFlowCoordinatorStateMachine { case (.roomList(let detailState), .deselectRoom): // Ignore the flow's dismissal if it has already been replaced with a space. return detailState == .space ? nil : .roomList(detailState: nil) - + case (.roomList, .startSpaceFlow): return .roomList(detailState: .space) case (.roomList, .finishedSpaceFlow): diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index f34b29caa..a631b9e98 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1606,6 +1606,12 @@ internal enum L10n { internal static var screenCreateRoomRoomAccessSectionKnockingOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_description") } /// Allow ask to join internal static var screenCreateRoomRoomAccessSectionKnockingOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_title") } + /// Anyone in %1$@ can join but everyone else must request access. + internal static func screenCreateRoomRoomAccessSectionKnockingRestrictedOptionDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_restricted_option_description", String(describing: p1)) + } + /// Ask to join + internal static var screenCreateRoomRoomAccessSectionKnockingRestrictedOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_restricted_option_title") } /// Only people invited can join. internal static var screenCreateRoomRoomAccessSectionPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_private_option_description") } /// Private @@ -1614,6 +1620,12 @@ internal enum L10n { internal static var screenCreateRoomRoomAccessSectionPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_public_option_description") } /// Public internal static var screenCreateRoomRoomAccessSectionPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_public_option_title") } + /// Anyone in %1$@ can join. + internal static func screenCreateRoomRoomAccessSectionRestrictedOptionDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_create_room_room_access_section_restricted_option_description", String(describing: p1)) + } + /// Standard + internal static var screenCreateRoomRoomAccessSectionRestrictedOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_restricted_option_title") } /// Who has access internal static var screenCreateRoomRoomAccessSectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_title") } /// You’ll need an address in order to make it visible in the public directory. diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 46460c31d..5ea9b8946 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -16814,6 +16814,11 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable { set(value) { underlyingTopLevelSpacesPublisher = value } } var underlyingTopLevelSpacesPublisher: CurrentValuePublisher<[SpaceServiceRoomProtocol], Never>! + var spaceFilterPublisher: CurrentValuePublisher<[SpaceServiceFilter], Never> { + get { return underlyingSpaceFilterPublisher } + set(value) { underlyingSpaceFilterPublisher = value } + } + var underlyingSpaceFilterPublisher: CurrentValuePublisher<[SpaceServiceFilter], Never>! //MARK: - spaceRoomList diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index 1cbfc3a59..f599f2658 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -51,7 +51,7 @@ extension RoomSummaryProviderMock { } roomListSubject.send(rooms) - case let .all(filters): + case let .rooms(_, filters), let .all(filters): var rooms = initialRooms if filters.count > 1 { diff --git a/ElementX/Sources/Mocks/SpaceRoomInfoMock.swift b/ElementX/Sources/Mocks/SpaceRoomInfoMock.swift index b1feb1955..414adc6f3 100644 --- a/ElementX/Sources/Mocks/SpaceRoomInfoMock.swift +++ b/ElementX/Sources/Mocks/SpaceRoomInfoMock.swift @@ -60,6 +60,7 @@ extension [SpaceServiceRoomProtocol] { isSpace: true, childrenCount: 1, joinedMembersCount: 500, + canonicalAlias: "#the-foundation:matrix.org", state: .joined)), SpaceServiceRoomMock(.init(id: "space2", name: "The Second Foundation", @@ -72,6 +73,7 @@ extension [SpaceServiceRoomProtocol] { isSpace: true, childrenCount: 25000, joinedMembersCount: 1_000_000_000, + canonicalAlias: "#the-galactic-empire:matrix.org", state: .joined)), SpaceServiceRoomMock(.init(id: "space4", name: "The Korellians", diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index 2c84e8681..fd10fd1dc 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -14,6 +14,7 @@ import MatrixRustSDKMocks extension SpaceServiceProxyMock { struct Configuration { var topLevelSpaces: [SpaceServiceRoomProtocol] = [] + var spaceFilters: [SpaceServiceFilter] = [] var joinedParentSpaces: [SpaceServiceRoomProtocol] = [] var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:] var leaveSpaceRooms: [LeaveSpaceRoom] = [] @@ -23,6 +24,8 @@ extension SpaceServiceProxyMock { self.init() topLevelSpacesPublisher = .init(configuration.topLevelSpaces) + spaceFilterPublisher = .init(configuration.spaceFilters) + joinedParentsChildIDReturnValue = .success(configuration.joinedParentSpaces) spaceRoomListSpaceIDClosure = { spaceID in if let spaceRoomList = configuration.spaceRoomLists[spaceID] { @@ -31,6 +34,7 @@ extension SpaceServiceProxyMock { .failure(.sdkError(ClientProxyMockError.generic)) } } + leaveSpaceSpaceIDClosure = { spaceID in .success(LeaveSpaceHandleProxy(spaceID: spaceID, leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms)))) @@ -45,13 +49,21 @@ extension SpaceServiceProxyMock { extension SpaceServiceProxyMock.Configuration { static var populated: SpaceServiceProxyMock.Configuration { + let spaceFilters = [SpaceServiceRoomProtocol].mockJoinedSpaces.reduce(into: [SpaceServiceFilter]()) { partialResult, spaceRoom in + partialResult.append(SpaceServiceFilter(room: spaceRoom, level: 0, descendants: .init())) + partialResult.append(SpaceServiceFilter(room: spaceRoom, level: 1, descendants: .init())) + } + let spaceRoomLists = [SpaceServiceRoomProtocol].mockJoinedSpaces.map { ($0.id, SpaceRoomListProxyMock(.init(spaceServiceRoom: $0, initialSpaceRooms: .mockSpaceList))) } + let subSpaceRoomLists = [SpaceServiceRoomProtocol].mockSpaceList.map { ($0.id, SpaceRoomListProxyMock(.init(spaceServiceRoom: $0, initialSpaceRooms: .mockSingleRoom))) } - return .init(topLevelSpaces: .mockJoinedSpaces, spaceRoomLists: .init(uniqueKeysWithValues: spaceRoomLists + subSpaceRoomLists)) + return .init(topLevelSpaces: .mockJoinedSpaces, + spaceFilters: spaceFilters, + spaceRoomLists: .init(uniqueKeysWithValues: spaceRoomLists + subSpaceRoomLists)) } } diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 85a92c587..0058e6108 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -108,6 +108,7 @@ enum A11yIdentifiers { let userAvatar = "home_screen-user_avatar" let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue" let startChat = "home_screen-start_chat" + let spaceFilters = "home_screen-space_filters" let roomNamePrefix = "home_screen-room_name" func roomName(_ name: String) -> String { diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 4e558e308..4591f798e 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -141,6 +141,7 @@ enum RoomAvatarSizeOnScreen { case chats case spaces case spaceSettings + case spaceFilters case authorizedSpaces case timeline case leaveSpace @@ -161,7 +162,7 @@ enum RoomAvatarSizeOnScreen { case .chats, .spaces, .spaceSettings: return 52 case .timeline, .leaveSpace, .roomDirectorySearch, - .completionSuggestions, .authorizedSpaces: + .completionSuggestions, .authorizedSpaces, .spaceFilters: return 32 case .notificationSettings: return 30 diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index c6a9159f7..2bdf6beaa 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -113,6 +113,10 @@ extension SDKListener: SpaceRoomListSpaceListener where T == SpaceRoom? { func onUpdate(space: SpaceRoom?) { onUpdateClosure(space) } } +extension SDKListener: SpaceServiceSpaceFiltersListener where T == [SpaceFilterUpdate] { + func onUpdate(filterUpdates: [SpaceFilterUpdate]) { onUpdateClosure(filterUpdates) } +} + // MARK: Room extension SDKListener: RoomInfoListener where T == RoomInfo { diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index ce936c485..0b7391292 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -26,6 +26,8 @@ enum TestablePreviewsDictionary { "BugReportScreen_Previews" : BugReportScreen_Previews.self, "CallInviteRoomTimelineView_Previews" : CallInviteRoomTimelineView_Previews.self, "CallNotificationRoomTimelineView_Previews" : CallNotificationRoomTimelineView_Previews.self, + "ChatsSpaceFilterCell_Previews" : ChatsSpaceFilterCell_Previews.self, + "ChatsSpaceFiltersScreen_Previews" : ChatsSpaceFiltersScreen_Previews.self, "CollapsibleRoomTimelineView_Previews" : CollapsibleRoomTimelineView_Previews.self, "CompletionSuggestion_Previews" : CompletionSuggestion_Previews.self, "ComposerToolbar_Previews" : ComposerToolbar_Previews.self, diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenModels.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenModels.swift new file mode 100644 index 000000000..c5323e0f2 --- /dev/null +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenModels.swift @@ -0,0 +1,32 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum ChatsSpaceFiltersScreenViewModelAction { + case confirm(SpaceServiceFilter) + case cancel +} + +struct ChatsSpaceFiltersScreenViewState: BindableState { + var filters = [SpaceServiceFilter]() + var bindings: ChatsSpaceFiltersScreenViewStateBindings +} + +struct ChatsSpaceFiltersScreenViewStateBindings { } + +enum ChatsSpaceFiltersScreenViewAction: CustomStringConvertible { + case confirm(SpaceServiceFilter) + case cancel + + var description: String { + switch self { + case .confirm: "Confirm" + case .cancel: "Cancel" + } + } +} diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModel.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModel.swift new file mode 100644 index 000000000..ae091d4e4 --- /dev/null +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModel.swift @@ -0,0 +1,50 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias ChatsSpaceFiltersScreenViewModelType = StateStoreViewModelV2 + +class ChatsSpaceFiltersScreenViewModel: ChatsSpaceFiltersScreenViewModelType, ChatsSpaceFiltersScreenViewModelProtocol, Identifiable { + private let spaceService: SpaceServiceProxyProtocol + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + let id = UUID() + + init(spaceService: SpaceServiceProxyProtocol, + mediaProvider: MediaProviderProtocol) { + self.spaceService = spaceService + + super.init(initialViewState: ChatsSpaceFiltersScreenViewState(bindings: .init()), + mediaProvider: mediaProvider) + + state.filters = spaceService.spaceFilterPublisher.value + + spaceService.spaceFilterPublisher.sink { [weak self] filters in + self?.state.filters = filters + } + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: ChatsSpaceFiltersScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .confirm(let filter): + actionsSubject.send(.confirm(filter)) + case .cancel: + actionsSubject.send(.cancel) + } + } +} diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModelProtocol.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModelProtocol.swift new file mode 100644 index 000000000..f0b5ab66c --- /dev/null +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/ChatsSpaceFiltersScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine + +@MainActor +protocol ChatsSpaceFiltersScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: ChatsSpaceFiltersScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift new file mode 100644 index 000000000..2aea1efe6 --- /dev/null +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift @@ -0,0 +1,104 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import Compound +import SwiftUI + +struct ChatsSpaceFilterCell: View { + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + let filter: SpaceServiceFilter + let mediaProvider: MediaProviderProtocol! + + private let verticalInsets = 12.0 + private let horizontalInsets = 16.0 + + let action: (SpaceServiceFilter) -> Void + + var body: some View { + Button { + action(filter) + } label: { + HStack(spacing: 12.0) { + HStack(spacing: 8.0) { + if filter.level > 0 { + Spacer(minLength: 16 * CGFloat(filter.level)) + } + + HStack(spacing: 12.0) { + avatar + + content + .padding(.vertical, verticalInsets) + .overlay(alignment: .bottom) { + Rectangle() + .fill(Color.compound.borderDisabled) + .frame(height: 1 / UIScreen.main.scale) + .padding(.trailing, -horizontalInsets) + } + } + .accessibilityElement(children: .combine) + } + } + } + .padding(.horizontal, horizontalInsets) + } + + @ViewBuilder @MainActor + private var avatar: some View { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: filter.room.avatar, + avatarSize: .room(on: .spaceFilters), + mediaProvider: mediaProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + } + + private var content: some View { + HStack(spacing: 16) { + VStack(alignment: .leading, spacing: 2) { + Text(filter.room.name) + .font(.compound.bodyLG) + .foregroundColor(.compound.textPrimary) + .lineLimit(1) + + ZStack { + // Hidden text to maintain consistent height. + Text("") + .hidden() + + if let alias = filter.room.canonicalAlias { + Text(alias) + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + .lineLimit(1) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +struct ChatsSpaceFilterCell_Previews: PreviewProvider, TestablePreview { + static let mediaProvider = MediaProviderMock(configuration: .init()) + + static let spaces = [SpaceServiceRoomProtocol].mockJoinedSpaces2 + + static var previews: some View { + VStack(spacing: 0) { + ForEach(spaces, id: \.id) { space in + ChatsSpaceFilterCell(filter: .init(room: space, level: 0, descendants: .init()), + mediaProvider: mediaProvider) { _ in } + ChatsSpaceFilterCell(filter: .init(room: space, level: 1, descendants: .init()), + mediaProvider: mediaProvider) { _ in } + } + } + } +} diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFiltersScreen.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFiltersScreen.swift new file mode 100644 index 000000000..1ec71b50e --- /dev/null +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFiltersScreen.swift @@ -0,0 +1,56 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct ChatsSpaceFiltersScreen: View { + @Bindable var context: ChatsSpaceFiltersScreenViewModel.Context + + var body: some View { + NavigationStack { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(context.viewState.filters) { filter in + ChatsSpaceFilterCell(filter: filter, + mediaProvider: context.mediaProvider) { filter in + context.send(viewAction: .confirm(filter)) + } + } + } + } + .toolbar { toolbar } + .navigationTitle(L10n.screenRoomlistYourSpaces) + .navigationBarTitleDisplayMode(.inline) + } + .presentationDragIndicator(.visible) + } + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + ToolbarButton(role: .cancel) { + context.send(viewAction: .cancel) + } + } + } +} + +// MARK: - Previews + +struct ChatsSpaceFiltersScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = makeViewModel() + + static var previews: some View { + ChatsSpaceFiltersScreen(context: viewModel.context) + } + + static func makeViewModel() -> ChatsSpaceFiltersScreenViewModel { + ChatsSpaceFiltersScreenViewModel(spaceService: SpaceServiceProxyMock(.populated), + mediaProvider: MediaProviderMock(configuration: .init())) + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 2f8480131..d3d4431fd 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -43,6 +43,7 @@ enum HomeScreenViewAction { case dismissNewSoundBanner case updateVisibleItemRange(Range) case globalSearch + case spaceFilters case markRoomAsUnread(roomIdentifier: String) case markRoomAsRead(roomIdentifier: String) case markRoomAsFavourite(roomIdentifier: String, isFavourite: Bool) @@ -109,6 +110,10 @@ struct HomeScreenViewState: BindableState { var reportRoomEnabled = false + var spaceFiltersEnabled = false + + var selectedSpaceFilter: SpaceServiceFilter? + var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { return placeholderRooms @@ -150,6 +155,8 @@ struct HomeScreenViewStateBindings { var alertInfo: AlertInfo? var leaveRoomAlertItem: LeaveRoomAlertItem? + + var spaceFiltersViewModel: ChatsSpaceFiltersScreenViewModel? } struct HomeScreenRoom: Identifiable, Equatable { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index f33897982..e0b5df320 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -15,6 +15,7 @@ typealias HomeScreenViewModelType = StateStoreViewModel private let analyticsService: AnalyticsService private let appSettings: AppSettings private let notificationManager: NotificationManagerProtocol @@ -39,9 +40,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol self.notificationManager = notificationManager self.userIndicatorController = userIndicatorController + spaceFilterSubject = CurrentValueSubject(nil) + roomSummaryProvider = userSession.clientProxy.roomSummaryProvider super.init(initialViewState: .init(userID: userSession.clientProxy.userID, + spaceFiltersEnabled: appSettings.spaceFiltersEnabled, bindings: .init(filtersState: .init(appSettings: appSettings))), mediaProvider: userSession.mediaProvider) @@ -112,12 +116,22 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } .store(in: &cancellables) + appSettings.$spaceFiltersEnabled + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.spaceFiltersEnabled, on: self) + .store(in: &cancellables) + userSession.clientProxy.hideInviteAvatarsPublisher .removeDuplicates() .receive(on: DispatchQueue.main) .weakAssign(to: \.state.hideInviteAvatars, on: self) .store(in: &cancellables) + spaceFilterSubject + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.selectedSpaceFilter, on: self) + .store(in: &cancellables) + Task { state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported } @@ -126,9 +140,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol let searchQuery = context.$viewState.map(\.bindings.searchQuery) let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters) isSearchFieldFocused - .combineLatest(searchQuery, activeFilters) + .combineLatest(searchQuery, activeFilters, spaceFilterSubject) .removeDuplicates { $0 == $1 } - .sink { [weak self] isSearchFieldFocused, _, _ in + .sink { [weak self] isSearchFieldFocused, _, _, _ in guard let self else { return } // isSearchFieldFocused` is sometimes turning to true after cancelling the search. So to be extra sure we are updating the values correctly we read them directly in the next run loop, and we add a small delay if the value has changed let delay = isSearchFieldFocused == self.context.viewState.bindings.isSearchFieldFocused ? 0.0 : 0.05 @@ -175,6 +189,26 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol actionsSubject.send(.presentStartChatScreen) case .globalSearch: actionsSubject.send(.presentGlobalSearch) + case .spaceFilters: + if spaceFilterSubject.value != nil { + spaceFilterSubject.send(nil) + } else { + state.bindings.spaceFiltersViewModel = ChatsSpaceFiltersScreenViewModel(spaceService: userSession.clientProxy.spaceService, + mediaProvider: userSession.mediaProvider) + + state.bindings.spaceFiltersViewModel?.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .confirm(let spaceServiceFilter): + spaceFilterSubject.send(spaceServiceFilter) + state.bindings.spaceFiltersViewModel = nil + case .cancel: + state.bindings.spaceFiltersViewModel = nil + } + } + .store(in: &cancellables) + } case .markRoomAsUnread(let roomIdentifier): Task { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomIdentifier) else { @@ -242,7 +276,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol if state.bindings.isSearchFieldFocused { roomSummaryProvider?.setFilter(.search(query: state.bindings.searchQuery)) } else { - roomSummaryProvider?.setFilter(.all(filters: state.bindings.filtersState.activeFilters.set)) + if let spaceFilter = spaceFilterSubject.value { + roomSummaryProvider?.setFilter(.rooms(roomsIDs: spaceFilter.descendants, + filters: state.bindings.filtersState.activeFilters.set)) + } else { + roomSummaryProvider?.setFilter(.all(filters: state.bindings.filtersState.activeFilters.set)) + } } } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 80a0bb31a..3ab63ce34 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -16,21 +16,39 @@ struct HomeScreen: View { @State private var scrollViewAdapter = ScrollViewAdapter() + @Namespace private var navigationTransitionNamespace + private enum NavigationTransitionSourceID { + case spaceFilters + } + var body: some View { HomeScreenContent(context: context, scrollViewAdapter: scrollViewAdapter) .alert(item: $context.alertInfo) .alert(item: $context.leaveRoomAlertItem, actions: leaveRoomAlertActions, message: leaveRoomAlertMessage) - .navigationTitle(L10n.screenRoomlistMainSpaceTitle) + .navigationTitle(title) .toolbar { toolbar } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .track(screen: .Home) .toolbarBloom(hasSearchBar: true) .sentryTrace("\(Self.self)") + .sheet(item: $context.spaceFiltersViewModel) { vm in + ChatsSpaceFiltersScreen(context: vm.context) + .navigationTransition(.zoom(sourceID: NavigationTransitionSourceID.spaceFilters, + in: navigationTransitionNamespace)) + } } // MARK: - Private + + private var title: String { + if let selectedSpace = context.viewState.selectedSpaceFilter { + selectedSpace.room.name + } else { + L10n.screenRoomlistMainSpaceTitle + } + } @ToolbarContentBuilder private var toolbar: some ToolbarContent { @@ -47,6 +65,20 @@ struct HomeScreen: View { .buttonStyle(.compound(.super, size: .toolbarIcon)) } } + + if context.viewState.spaceFiltersEnabled { + if #available(iOS 26, *) { + ToolbarSpacer(.fixed, placement: .primaryAction) + } + + ToolbarItem(placement: .primaryAction) { + SpaceFiltersButton(selected: context.viewState.selectedSpaceFilter != nil) { + context.send(viewAction: .spaceFilters) + } + .matchedTransitionSource(id: NavigationTransitionSourceID.spaceFilters, + in: navigationTransitionNamespace) + } + } } private var settingsButton: some View { @@ -93,6 +125,42 @@ struct HomeScreen: View { private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View { Text(item.subtitle) } + + private struct SpaceFiltersButton: View { + var selected = false + var action: () -> Void + + var body: some View { + if #available(iOS 26, *) { + if selected { + content + .backportButtonStyleGlassProminent() + .tint(.compound.bgAccentRest) + } else { + content + } + } else { + if selected { + content + .buttonStyle(.compound(.primary, size: .toolbarIcon)) + } else { + content + .buttonStyle(.compound(.tertiary, size: .toolbarIcon)) + } + } + } + + private var content: some View { + Button { + action() + } label: { + CompoundIcon(\.filter) + } + .accessibilityLabel(L10n.screenRoomlistYourSpaces) + .accessibilityAddTraits(selected ? .isSelected : []) + .accessibilityIdentifier(A11yIdentifiers.homeScreen.spaceFilters) + } + } } // MARK: - Previews diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 295caa43e..6b8091eea 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -70,6 +70,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var spaceSettingsEnabled: Bool { get set } var createSpaceEnabled: Bool { get set } + var spaceFiltersEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 99bb92b4f..8dd817bef 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -52,6 +52,10 @@ struct DeveloperOptionsScreen: View { } Section("Spaces") { + Toggle(isOn: $context.spaceFiltersEnabled) { + Text("Space filters") + } + Toggle(isOn: $context.spaceSettingsEnabled) { Text("Space settings") } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index ae3bc094f..800e1db4a 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -133,6 +133,18 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { [.normalizedMatchRoomName(pattern: query)] + baseFilter } _ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: filters)) + case .rooms(let roomIDs, let filters): + var rustFilters = filters.map(\.rustFilter) + baseFilter + + if !roomIDs.isEmpty { + rustFilters.append(.identifiers(identifiers: Array(roomIDs))) + } + + if !filters.contains(.lowPriority), appSettings.lowPriorityFilterEnabled { + rustFilters.append(.nonLowPriority) + } + + _ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: rustFilters)) case let .all(filters): var rustFilters = filters.map(\.rustFilter) + baseFilter diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift index 949f281d2..659f9adb5 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift @@ -40,6 +40,8 @@ enum RoomSummaryProviderFilter: Equatable { case search(query: String) /// Includes only what satisfies the filters used case all(filters: Set) + /// Include only rooms from the given that satisfy the given filters + case rooms(roomsIDs: Set, filters: Set) } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift index b20b05165..74dca3467 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift @@ -19,6 +19,12 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { spacesSubject.asCurrentValuePublisher() } + private var spaceFilterHandle: TaskHandle? + private let spaceFilterSubject = CurrentValueSubject<[SpaceServiceFilter], Never>([]) + var spaceFilterPublisher: CurrentValuePublisher<[SpaceServiceFilter], Never> { + spaceFilterSubject.asCurrentValuePublisher() + } + init(spaceService: SpaceServiceProtocol) { self.spaceService = spaceService @@ -27,7 +33,11 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { private func setupSubscriptions() async { topLevelSpacesHandle = await spaceService.subscribeToTopLevelJoinedSpaces(listener: SDKListener { [weak self] updates in - self?.handleUpdates(updates) + self?.handleSpaceListUpdates(updates) + }) + + spaceFilterHandle = await spaceService.subscribeToSpaceFilters(listener: SDKListener { [weak self] updates in + self?.handleSpaceFilterUpdates(updates) }) } @@ -87,7 +97,7 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { // MARK: - Private - private func handleUpdates(_ updates: [SpaceListUpdate]) { + private func handleSpaceListUpdates(_ updates: [SpaceListUpdate]) { var spaces = spacesSubject.value for update in updates { @@ -119,4 +129,37 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { spacesSubject.send(spaces) } + + private func handleSpaceFilterUpdates(_ updates: [SpaceFilterUpdate]) { + var filters = spaceFilterSubject.value + + for update in updates { + switch update { + case .append(let spaceFilters): + filters.append(contentsOf: spaceFilters.map(SpaceServiceFilter.init)) + case .clear: + filters.removeAll() + case .pushFront(let filter): + filters.insert(SpaceServiceFilter(filter: filter), at: 0) + case .pushBack(let filter): + filters.append(SpaceServiceFilter(filter: filter)) + case .popFront: + filters.removeFirst() + case .popBack: + filters.removeLast() + case .insert(let index, let filter): + filters.insert(SpaceServiceFilter(filter: filter), at: Int(index)) + case .set(let index, let filter): + filters[Int(index)] = SpaceServiceFilter(filter: filter) + case .remove(let index): + filters.remove(at: Int(index)) + case .truncate(let length): + filters.removeSubrange(Int(length).. + + init(room: SpaceServiceRoomProtocol, level: UInt, descendants: Set) { + self.room = room + self.level = level + self.descendants = descendants + } + + init(filter: SpaceFilter) { + room = SpaceServiceRoom(spaceRoom: filter.spaceRoom) + level = UInt(max(filter.level, 0)) + descendants = Set(filter.descendants) + } + + // Same rooms might appear on multiple levels + var id: String { + room.id + "\(level)" + } + + static func == (lhs: SpaceServiceFilter, rhs: SpaceServiceFilter) -> Bool { + lhs.room.id == rhs.room.id + } +} + // sourcery: AutoMockable protocol SpaceServiceProxyProtocol { var topLevelSpacesPublisher: CurrentValuePublisher<[SpaceServiceRoomProtocol], Never> { get } + var spaceFilterPublisher: CurrentValuePublisher<[SpaceServiceFilter], Never> { get } func spaceRoomList(spaceID: String) async -> Result /// Returns a joined space given its identifier diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 3ae586465..229d13e4f 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -130,6 +130,20 @@ extension PreviewTests { } } + func testChatsSpaceFilterCell() async throws { + AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings. + for (index, preview) in ChatsSpaceFilterCell_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + + func testChatsSpaceFiltersScreen() async throws { + AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings. + for (index, preview) in ChatsSpaceFiltersScreen_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testCollapsibleRoomTimelineView() async throws { AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings. for (index, preview) in CollapsibleRoomTimelineView_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-en-GB-0.png new file mode 100644 index 000000000..d7d313960 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d3f226d34b1e0bd2297f4359189c8b33223316ea7dfb685bc6de866bb58e5ff +size 142344 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-pseudo-0.png new file mode 100644 index 000000000..d7d313960 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d3f226d34b1e0bd2297f4359189c8b33223316ea7dfb685bc6de866bb58e5ff +size 142344 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-en-GB-0.png new file mode 100644 index 000000000..b18baa3a5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8abbfad872d996a2f82668373b92f0f33ec51b637263deb14c655d0cb958606 +size 93320 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-pseudo-0.png new file mode 100644 index 000000000..b18baa3a5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFilterCell.iPhone-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8abbfad872d996a2f82668373b92f0f33ec51b637263deb14c655d0cb958606 +size 93320 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-en-GB-0.png new file mode 100644 index 000000000..0d03b677b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4bb60e28bcde15e69c627fa4b5f6dca2b26a3f25cbf1afb62a9302d6d549b46 +size 150715 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-pseudo-0.png new file mode 100644 index 000000000..0d03b677b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4bb60e28bcde15e69c627fa4b5f6dca2b26a3f25cbf1afb62a9302d6d549b46 +size 150715 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-en-GB-0.png new file mode 100644 index 000000000..85b33fee1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e28fb24e8a9087f8e951ed8c18ab9048cfacd72fcb2c328ddf3e46169306a1b6 +size 100972 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-pseudo-0.png new file mode 100644 index 000000000..85b33fee1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/chatsSpaceFiltersScreen.iPhone-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e28fb24e8a9087f8e951ed8c18ab9048cfacd72fcb2c328ddf3e46169306a1b6 +size 100972 diff --git a/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift b/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift index 55ce23831..d14160889 100644 --- a/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift +++ b/SDKMocks/Sources/Generated/SDKGeneratedMocks.swift @@ -17125,6 +17125,52 @@ open class RoomSDKMock: MatrixRustSDK.Room, @unchecked Sendable { try await setNameNameClosure?(name) } + //MARK: - setOwnMemberDisplayName + + open var setOwnMemberDisplayNameDisplayNameThrowableError: Error? + open var setOwnMemberDisplayNameDisplayNameUnderlyingCallsCount = 0 + open var setOwnMemberDisplayNameDisplayNameCallsCount: Int { + get { + if Thread.isMainThread { + return setOwnMemberDisplayNameDisplayNameUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = setOwnMemberDisplayNameDisplayNameUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + setOwnMemberDisplayNameDisplayNameUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + setOwnMemberDisplayNameDisplayNameUnderlyingCallsCount = newValue + } + } + } + } + open var setOwnMemberDisplayNameDisplayNameCalled: Bool { + return setOwnMemberDisplayNameDisplayNameCallsCount > 0 + } + open var setOwnMemberDisplayNameDisplayNameReceivedDisplayName: String? + open var setOwnMemberDisplayNameDisplayNameReceivedInvocations: [String?] = [] + open var setOwnMemberDisplayNameDisplayNameClosure: ((String?) async throws -> Void)? + + open override func setOwnMemberDisplayName(displayName: String?) async throws { + if let error = setOwnMemberDisplayNameDisplayNameThrowableError { + throw error + } + setOwnMemberDisplayNameDisplayNameCallsCount += 1 + setOwnMemberDisplayNameDisplayNameReceivedDisplayName = displayName + DispatchQueue.main.async { + self.setOwnMemberDisplayNameDisplayNameReceivedInvocations.append(displayName) + } + try await setOwnMemberDisplayNameDisplayNameClosure?(displayName) + } + //MARK: - setThreadSubscription open var setThreadSubscriptionThreadRootEventIdSubscribedThrowableError: Error? @@ -23601,6 +23647,71 @@ open class SpaceServiceSDKMock: MatrixRustSDK.SpaceService, @unchecked Sendable try await removeChildFromSpaceChildIdSpaceIdClosure?(childId, spaceId) } + //MARK: - spaceFilters + + open var spaceFiltersUnderlyingCallsCount = 0 + open var spaceFiltersCallsCount: Int { + get { + if Thread.isMainThread { + return spaceFiltersUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = spaceFiltersUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + spaceFiltersUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + spaceFiltersUnderlyingCallsCount = newValue + } + } + } + } + open var spaceFiltersCalled: Bool { + return spaceFiltersCallsCount > 0 + } + + open var spaceFiltersUnderlyingReturnValue: [SpaceFilter]! + open var spaceFiltersReturnValue: [SpaceFilter]! { + get { + if Thread.isMainThread { + return spaceFiltersUnderlyingReturnValue + } else { + var returnValue: [SpaceFilter]? = nil + DispatchQueue.main.sync { + returnValue = spaceFiltersUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + spaceFiltersUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + spaceFiltersUnderlyingReturnValue = newValue + } + } + } + } + open var spaceFiltersClosure: (() async -> [SpaceFilter])? + + open override func spaceFilters() async -> [SpaceFilter] { + spaceFiltersCallsCount += 1 + if let spaceFiltersClosure = spaceFiltersClosure { + return await spaceFiltersClosure() + } else { + return spaceFiltersReturnValue + } + } + //MARK: - spaceRoomList open var spaceRoomListSpaceIdThrowableError: Error? @@ -23676,6 +23787,77 @@ open class SpaceServiceSDKMock: MatrixRustSDK.SpaceService, @unchecked Sendable } } + //MARK: - subscribeToSpaceFilters + + open var subscribeToSpaceFiltersListenerUnderlyingCallsCount = 0 + open var subscribeToSpaceFiltersListenerCallsCount: Int { + get { + if Thread.isMainThread { + return subscribeToSpaceFiltersListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = subscribeToSpaceFiltersListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToSpaceFiltersListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + subscribeToSpaceFiltersListenerUnderlyingCallsCount = newValue + } + } + } + } + open var subscribeToSpaceFiltersListenerCalled: Bool { + return subscribeToSpaceFiltersListenerCallsCount > 0 + } + open var subscribeToSpaceFiltersListenerReceivedListener: SpaceServiceSpaceFiltersListener? + open var subscribeToSpaceFiltersListenerReceivedInvocations: [SpaceServiceSpaceFiltersListener] = [] + + open var subscribeToSpaceFiltersListenerUnderlyingReturnValue: TaskHandle! + open var subscribeToSpaceFiltersListenerReturnValue: TaskHandle! { + get { + if Thread.isMainThread { + return subscribeToSpaceFiltersListenerUnderlyingReturnValue + } else { + var returnValue: TaskHandle? = nil + DispatchQueue.main.sync { + returnValue = subscribeToSpaceFiltersListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToSpaceFiltersListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + subscribeToSpaceFiltersListenerUnderlyingReturnValue = newValue + } + } + } + } + open var subscribeToSpaceFiltersListenerClosure: ((SpaceServiceSpaceFiltersListener) async -> TaskHandle)? + + open override func subscribeToSpaceFilters(listener: SpaceServiceSpaceFiltersListener) async -> TaskHandle { + subscribeToSpaceFiltersListenerCallsCount += 1 + subscribeToSpaceFiltersListenerReceivedListener = listener + DispatchQueue.main.async { + self.subscribeToSpaceFiltersListenerReceivedInvocations.append(listener) + } + if let subscribeToSpaceFiltersListenerClosure = subscribeToSpaceFiltersListenerClosure { + return await subscribeToSpaceFiltersListenerClosure(listener) + } else { + return subscribeToSpaceFiltersListenerReturnValue + } + } + //MARK: - subscribeToTopLevelJoinedSpaces open var subscribeToTopLevelJoinedSpacesListenerUnderlyingCallsCount = 0 diff --git a/UnitTests/Sources/RoomSummaryProviderTests.swift b/UnitTests/Sources/RoomSummaryProviderTests.swift index d6f92638e..b5258a384 100644 --- a/UnitTests/Sources/RoomSummaryProviderTests.swift +++ b/UnitTests/Sources/RoomSummaryProviderTests.swift @@ -82,6 +82,25 @@ final class RoomSummaryProviderTests: XCTestCase { .all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority])) } + func testRoomIdentifierFilters() async { + setupProvider() + await Task.yield() + + // Then it should have the default Rust filters enabled. + XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1) + XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, + .all(filters: baseFilters)) + + // When setting one our user filters. + roomSummaryProvider.setFilter(.rooms(roomsIDs: ["SomeRoom"], filters: [.favourites])) + await Task.yield() + + // Then that filter should be added to the default Rust filters. + XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2) + XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last, + .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])])) + } + // MARK: - Helpers private func setupProvider(isLowPriorityFilterEnabled: Bool = false) {