diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 9ad3bb8a6..f40724830 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -391,6 +391,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "MessageText_Previews") } + func testModerationAndSafetySettingsScreen() async throws { + try await performAccessibilityAudit(named: "ModerationAndSafetySettingsScreen_Previews") + } + func testMultiAccountAnnouncementScreen() async throws { try await performAccessibilityAudit(named: "MultiAccountAnnouncementScreen_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index cc356666f..acd1186a7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 0AD8EF040A60D62F488C18B5 /* KnockRequestProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0B05A35FF5D1ED9E8A0B41A7 /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */; }; + 0B13FACE07CF4F4DED62F811 /* ModerationAndSafetySettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C4CCE4419A369F0A6FD9A3 /* ModerationAndSafetySettingsScreen.swift */; }; 0B30DC7D9D445C405ADCB5FA /* VolatileUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3994D186AC9893BEE458ECF /* VolatileUserDefaults.swift */; }; 0B9436A91CFA658202400465 /* WaitingConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E21DC54FCC2A7F78532923F /* WaitingConfirmation.swift */; }; 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; }; @@ -147,6 +148,7 @@ 1524AAAE9BCAA32D65E97866 /* MapLibreInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 842F63AA68E8EB410B1BA29F /* MapLibreInterface.framework */; }; 155063E980E763D4910EA3CF /* Analytics+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; + 156148ED5799EE85BEFF47E7 /* ModerationAndSafetySettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63A7C1486AA8FF088B1701C /* ModerationAndSafetySettingsScreenCoordinator.swift */; }; 1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; }; 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; }; 1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; @@ -562,6 +564,7 @@ 5787613A79C6D553DEA28C5F /* RoomThreadListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F91AC45CDDF85E946391CA5C /* RoomThreadListScreenViewModel.swift */; }; 5790B18A7953860715B89A8B /* InverseMask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BA4D3DFEA16CDD3FE99064 /* InverseMask.swift */; }; 583A41A4BE76E2E9E0B97881 /* ResolveVerifiedUserSendFailureScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */; }; + 5846723185B444D52E077874 /* ModerationAndSafetySettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B160231C967427FB8217C7 /* ModerationAndSafetySettingsScreenViewModel.swift */; }; 585DCA0487A0A6F4E59EF5CA /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353024006CB726E9F9187B3A /* AnalyticsConsentState.swift */; }; 5877255D6E6ED898D402ED8D /* LocationPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408ACC0D28656F82A5EB6A7E /* LocationPickerSheet.swift */; }; 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; @@ -717,6 +720,7 @@ 708B77D5E65BB48D44732236 /* AccountUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F0A9118FF25E18D5CD6526 /* AccountUserDefaults.swift */; }; 708FC3184CCED825F0A36273 /* RoomThreadListServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */; }; 709A9B52FC26B4CB86B8B020 /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */; }; + 70A4C56E4E6C60A3124631FC /* ModerationAndSafetySettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8465F252997056A1E094204E /* ModerationAndSafetySettingsScreenModels.swift */; }; 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */; }; 715FB2D768E8919979EACD71 /* StopButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A786DE47C0DBCE0CD163193 /* StopButton.swift */; }; 71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */; }; @@ -801,6 +805,7 @@ 7C545FFEC9930F7247352593 /* SecurityAndPrivacyScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978092B01BEAB39F2C4389AE /* SecurityAndPrivacyScreenViewModel.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; 7C81239BB5C79382812D6446 /* MapLibreInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 842F63AA68E8EB410B1BA29F /* MapLibreInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7C8F9E3AF56E7FF5B338DBFA /* ModerationAndSafetySettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EAB1C79CE591229A011677 /* ModerationAndSafetySettingsScreenViewModelProtocol.swift */; }; 7C9BDF1FC7BD46C4676536AB /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; 7D249465ED00988EEEC14E05 /* JoinedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */; }; @@ -2316,6 +2321,7 @@ 578AF9CE60816069536C0953 /* RoomRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRole.swift; sourceTree = ""; }; 57916A1578D8043BB0795441 /* GeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsTests.swift; sourceTree = ""; }; + 57B160231C967427FB8217C7 /* ModerationAndSafetySettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreenViewModel.swift; sourceTree = ""; }; 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = ""; }; 580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceHandleSDKMock.swift; sourceTree = ""; }; 584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; @@ -2400,6 +2406,7 @@ 6695C64F066628411EAD21E9 /* RoomPreviewProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPreviewProxyMock.swift; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; + 66C4CCE4419A369F0A6FD9A3 /* ModerationAndSafetySettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreen.swift; sourceTree = ""; }; 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = ""; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = ""; }; @@ -2570,6 +2577,7 @@ 842F63AA68E8EB410B1BA29F /* MapLibreInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MapLibreInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 8465F252997056A1E094204E /* ModerationAndSafetySettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreenModels.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 = ""; }; @@ -2768,6 +2776,7 @@ A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineFlowCoordinator.swift; sourceTree = ""; }; A566F8F2C27B99E1FB80C69B /* JoinRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRule.swift; sourceTree = ""; }; A5C45578F406698388B97C07 /* AuthenticationClassicAppAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClassicAppAccountView.swift; sourceTree = ""; }; + A63A7C1486AA8FF088B1701C /* ModerationAndSafetySettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreenCoordinator.swift; sourceTree = ""; }; A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProvider.swift; sourceTree = ""; }; A6B19D10B102956066AF117B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; @@ -2962,6 +2971,7 @@ C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = ""; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = ""; }; + C4EAB1C79CE591229A011677 /* ModerationAndSafetySettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreenViewModelProtocol.swift; sourceTree = ""; }; C537DE821FED94D23467B6C4 /* PillUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillUtilities.swift; sourceTree = ""; }; C55679AF67545EF8087E47BE /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = ""; }; C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; @@ -3832,6 +3842,14 @@ path = MediaUploadPreviewScreen; sourceTree = ""; }; + 237FAE99049935EE1B02E443 /* View */ = { + isa = PBXGroup; + children = ( + 66C4CCE4419A369F0A6FD9A3 /* ModerationAndSafetySettingsScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 24FBB8F76D6435033CD07747 /* ManageAuthorizedSpacesScreen */ = { isa = PBXGroup; children = ( @@ -5247,6 +5265,7 @@ CBB8EC37B802F3DD74A0205E /* DeactivateAccountScreen */, 1CA6CD0DE6F0445156361B6D /* DeveloperOptionsScreen */, 38A1C74493B816B8753F5BC2 /* LegalInformationScreen */, + 91D3BE26C39545233959CDCE /* ModerationAndSafetySettingsScreen */, 441115752CA2408F26A72D11 /* NotificationSettingsEditScreen */, 7B91CB64534AD870924CCFEF /* NotificationSettingsScreen */, B364E08924AD15820350CDD9 /* SettingsScreen */, @@ -5972,6 +5991,18 @@ path = Location; sourceTree = ""; }; + 91D3BE26C39545233959CDCE /* ModerationAndSafetySettingsScreen */ = { + isa = PBXGroup; + children = ( + A63A7C1486AA8FF088B1701C /* ModerationAndSafetySettingsScreenCoordinator.swift */, + 8465F252997056A1E094204E /* ModerationAndSafetySettingsScreenModels.swift */, + 57B160231C967427FB8217C7 /* ModerationAndSafetySettingsScreenViewModel.swift */, + C4EAB1C79CE591229A011677 /* ModerationAndSafetySettingsScreenViewModelProtocol.swift */, + 237FAE99049935EE1B02E443 /* View */, + ); + path = ModerationAndSafetySettingsScreen; + sourceTree = ""; + }; 92E99C57D7F92ED16F73282C /* ElementCall */ = { isa = PBXGroup; children = ( @@ -9179,6 +9210,11 @@ C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, + 0B13FACE07CF4F4DED62F811 /* ModerationAndSafetySettingsScreen.swift in Sources */, + 156148ED5799EE85BEFF47E7 /* ModerationAndSafetySettingsScreenCoordinator.swift in Sources */, + 70A4C56E4E6C60A3124631FC /* ModerationAndSafetySettingsScreenModels.swift in Sources */, + 5846723185B444D52E077874 /* ModerationAndSafetySettingsScreenViewModel.swift in Sources */, + 7C8F9E3AF56E7FF5B338DBFA /* ModerationAndSafetySettingsScreenViewModelProtocol.swift in Sources */, DD66C863B4232BCDEBE883C8 /* MultiAccountAnnouncementScreen.swift in Sources */, A0790A97DAD44E8895035704 /* MultiAccountAnnouncementScreenCoordinator.swift in Sources */, C90A5ABB868F7DDEE2DFD653 /* MultiAccountAnnouncementScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 221c350c0..be2653778 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -102,6 +102,7 @@ enum TestablePreviewsDictionary { "MessageComposer_Previews" : MessageComposer_Previews.self, "MessageForwardingScreen_Previews" : MessageForwardingScreen_Previews.self, "MessageText_Previews" : MessageText_Previews.self, + "ModerationAndSafetySettingsScreen_Previews" : ModerationAndSafetySettingsScreen_Previews.self, "MultiAccountAnnouncementScreen_Previews" : MultiAccountAnnouncementScreen_Previews.self, "NoticeRoomTimelineView_Previews" : NoticeRoomTimelineView_Previews.self, "NotificationPermissionsScreen_Previews" : NotificationPermissionsScreen_Previews.self, diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenCoordinator.swift new file mode 100644 index 000000000..bb632d908 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenCoordinator.swift @@ -0,0 +1,27 @@ +// +// Copyright 2026 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 + +struct ModerationAndSafetySettingsScreenCoordinatorParameters { + let userSession: UserSessionProtocol + let userIndicatorController: UserIndicatorControllerProtocol +} + +final class ModerationAndSafetySettingsScreenCoordinator: CoordinatorProtocol { + private var viewModel: ModerationAndSafetySettingsScreenViewModelProtocol + + init(parameters: ModerationAndSafetySettingsScreenCoordinatorParameters) { + viewModel = ModerationAndSafetySettingsScreenViewModel(userSession: parameters.userSession, + userIndicatorController: parameters.userIndicatorController) + } + + func toPresentable() -> AnyView { + AnyView(ModerationAndSafetySettingsScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift new file mode 100644 index 000000000..044e9e279 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift @@ -0,0 +1,44 @@ +// +// Copyright 2026 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 +import UIKit + +enum ModerationAndSafetySettingsScreenViewModelAction { + case blockedUsers +} + +struct ModerationAndSafetySettingsScreenViewState: BindableState { + var hideInviteAvatars: Bool + var isWaitingHideInviteAvatars = false + + var timelineMediaVisibility: TimelineMediaVisibility + var isWaitingTimelineMediaVisibility = false + + var showBlockedUsers = false + + var bindings: ModerationAndSafetySettingsScreenViewStateBindings +} + +struct ModerationAndSafetySettingsScreenViewStateBindings { + private let userSettings: UserSettings + + init(userSettings: UserSettings) { + self.userSettings = userSettings + } + + var sharePresence: Bool { + get { userSettings.sharePresence } + set { userSettings.sharePresence = newValue } + } +} + +enum ModerationAndSafetySettingsScreenViewAction { + case updateHideInviteAvatars(Bool) + case updateTimelineMediaVisibility(TimelineMediaVisibility) + case blockedUsers +} diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModel.swift new file mode 100644 index 000000000..b9aa2b7d3 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModel.swift @@ -0,0 +1,106 @@ +// +// Copyright 2026 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 ModerationAndSafetySettingsScreenViewModelType = StateStoreViewModelV2 + +class ModerationAndSafetySettingsScreenViewModel: ModerationAndSafetySettingsScreenViewModelType, ModerationAndSafetySettingsScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private var timelineMediaVisibilityTask: Task? + private var hideInviteAvatarsTask: Task? + + private var actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(userSession: UserSessionProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + clientProxy = userSession.clientProxy + self.userIndicatorController = userIndicatorController + + let state = ModerationAndSafetySettingsScreenViewState(hideInviteAvatars: clientProxy.hideInviteAvatarsPublisher.value, + timelineMediaVisibility: clientProxy.timelineMediaVisibilityPublisher.value, + bindings: .init(userSettings: userSession.userSettings)) + super.init(initialViewState: state) + + clientProxy.hideInviteAvatarsPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.hideInviteAvatars, on: self) + .store(in: &cancellables) + + clientProxy.timelineMediaVisibilityPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.timelineMediaVisibility, on: self) + .store(in: &cancellables) + + clientProxy.ignoredUsersPublisher + .receive(on: DispatchQueue.main) + .map { $0?.isEmpty == false } + .weakAssign(to: \.state.showBlockedUsers, on: self) + .store(in: &cancellables) + } + + override func process(viewAction: ModerationAndSafetySettingsScreenViewAction) { + switch viewAction { + case let .updateHideInviteAvatars(value): + hideInviteAvatarsTask = Task { [weak self] in await self?.updateHideInviteAvatars(value) } + case let .updateTimelineMediaVisibility(value): + timelineMediaVisibilityTask = Task { [weak self] in await self?.updateTimelineMediaVisibility(value) } + case .blockedUsers: + actionsSubject.send(.blockedUsers) + } + } + + private func updateTimelineMediaVisibility(_ value: TimelineMediaVisibility) async { + defer { + timelineMediaVisibilityTask = nil + state.isWaitingTimelineMediaVisibility = false + } + + let previousState = state.timelineMediaVisibility + state.isWaitingTimelineMediaVisibility = true + state.timelineMediaVisibility = value + // If the other value is updating wait also for it to finish + await hideInviteAvatarsTask?.value + + switch await clientProxy.setTimelineMediaVisibility(value) { + case .success: + break + case .failure: + state.timelineMediaVisibility = previousState + userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) + } + } + + private func updateHideInviteAvatars(_ value: Bool) async { + defer { + hideInviteAvatarsTask = nil + state.isWaitingHideInviteAvatars = false + } + + let previousState = state.hideInviteAvatars + state.isWaitingHideInviteAvatars = true + state.hideInviteAvatars = value + // If the other value is updating wait also for it to finish + await timelineMediaVisibilityTask?.value + + switch await clientProxy.setHideInviteAvatars(value) { + case .success: + break + case .failure: + state.hideInviteAvatars = previousState + userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) + } + } +} diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModelProtocol.swift new file mode 100644 index 000000000..4f4b7d318 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenViewModelProtocol.swift @@ -0,0 +1,13 @@ +// +// Copyright 2026 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 + +protocol ModerationAndSafetySettingsScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: ModerationAndSafetySettingsScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/View/ModerationAndSafetySettingsScreen.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/View/ModerationAndSafetySettingsScreen.swift new file mode 100644 index 000000000..5fdcbf3e6 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/View/ModerationAndSafetySettingsScreen.swift @@ -0,0 +1,113 @@ +// +// Copyright 2026 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 ModerationAndSafetySettingsScreen: View { + @Bindable var context: ModerationAndSafetySettingsScreenViewModel.Context + + var body: some View { + Form { + sharePresenceSection + invitesSection + timelineMediaSection + blockedUsersSection + } + .compoundList() + .navigationTitle(L10n.commonModerationAndSafety) + .navigationBarTitleDisplayMode(.inline) + } + + private var sharePresenceSection: some View { + Section { + ListRow(label: .plain(title: L10n.screenAdvancedSettingsSharePresence, + description: L10n.screenAdvancedSettingsSharePresenceDescription), + kind: .toggle($context.sharePresence)) + } header: { + Text(L10n.screenModerationAndSafetySharePresenceHeading) + .compoundListSectionHeader() + } + } + + @ViewBuilder + private var invitesSection: some View { + let binding = Binding(get: { + context.viewState.hideInviteAvatars + }, set: { newValue in + context.send(viewAction: .updateHideInviteAvatars(newValue)) + }) + + Section { + ListRow(label: .plain(title: L10n.screenAdvancedSettingsHideInviteAvatarsToggleTitle), + details: context.viewState.isWaitingHideInviteAvatars ? .isWaiting(true) : nil, + kind: .toggle(binding)) + .disabled(context.viewState.isWaitingHideInviteAvatars) + } header: { + Text(L10n.screenModerationAndSafetyInvitesHeading) + .compoundListSectionHeader() + } + } + + @ViewBuilder + private var timelineMediaSection: some View { + let binding = Binding(get: { + context.viewState.timelineMediaVisibility + }, set: { newValue in + context.send(viewAction: .updateTimelineMediaVisibility(newValue)) + }) + + Section { + ListRow(label: .plain(title: L10n.screenAdvancedSettingsShowMediaTimelineTitle), + details: .isWaiting(context.viewState.isWaitingTimelineMediaVisibility), + kind: .inlinePicker(selection: binding, + items: TimelineMediaVisibility.items)) + .disabled(context.viewState.isWaitingTimelineMediaVisibility) + } header: { + Text(L10n.screenAdvancedSettingsShowMediaTimelineTitle) + .compoundListSectionHeader() + } footer: { + Text(L10n.screenAdvancedSettingsShowMediaTimelineSubtitle) + .compoundListSectionFooter() + } + } + + @ViewBuilder + private var blockedUsersSection: some View { + if context.viewState.showBlockedUsers { + Section { + ListRow(label: .default(title: L10n.commonBlockedUsers, + icon: \.block), + kind: .navigationLink { + context.send(viewAction: .blockedUsers) + }) + } + } + } +} + +// MARK: - Previews + +struct ModerationAndSafetySettingsScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = ModerationAndSafetySettingsScreenViewModel(userSession: UserSessionMock(.init()), + userIndicatorController: UserIndicatorControllerMock()) + + static var previews: some View { + ElementNavigationStack { + ModerationAndSafetySettingsScreen(context: viewModel.context) + } + .snapshotPreferences(expect: viewModel.context.observe(\.viewState.showBlockedUsers)) + } +} + +private extension TimelineMediaVisibility { + static var items: [(title: String, tag: TimelineMediaVisibility)] { + [(title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysHide, tag: .never), + (title: L10n.screenAdvancedSettingsShowMediaTimelinePrivateRooms, tag: .privateOnly), + (title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysShow, tag: .always)] + } +} diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index deca02123..c0417ac13 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -683,6 +683,13 @@ extension PreviewTests { } } + @Test + func moderationAndSafetySettingsScreen() async throws { + for (index, preview) in ModerationAndSafetySettingsScreen_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + @Test func multiAccountAnnouncementScreen() async throws { for (index, preview) in MultiAccountAnnouncementScreen_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-en-GB-0.png new file mode 100644 index 000000000..5cc018cb0 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50408cdb214808e9b2965578b18d30ee1c41fc90c2d7ac406d0e41b0c66c697f +size 154427 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-pseudo-0.png new file mode 100644 index 000000000..7d3da16e5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68d850581286bd6b8f4bad6bf0466a7e8d5acb3cdbf898a02080a0ed1f3460f1 +size 178307 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-en-GB-0.png new file mode 100644 index 000000000..a826b421c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:260a0199e48eba6568ea52e2b71d9bf06607427e7a8136d16264fac2a1a8d7fb +size 103510 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-pseudo-0.png new file mode 100644 index 000000000..8b9ad6a5b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/moderationAndSafetySettingsScreen.iPhone-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbedf9d3402c7426cd5404141cd5901adb4ab2efe250299fbfc2bef77cd478bd +size 141937