mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add a ModerationAndSafetySettingsScreen. (#6278)
This commit is contained in:
@@ -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,
|
||||
|
||||
+27
@@ -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))
|
||||
}
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
+106
@@ -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<ModerationAndSafetySettingsScreenViewState, ModerationAndSafetySettingsScreenViewAction>
|
||||
|
||||
class ModerationAndSafetySettingsScreenViewModel: ModerationAndSafetySettingsScreenViewModelType, ModerationAndSafetySettingsScreenViewModelProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var timelineMediaVisibilityTask: Task<Void, Never>?
|
||||
private var hideInviteAvatarsTask: Task<Void, Never>?
|
||||
|
||||
private var actionsSubject: PassthroughSubject<ModerationAndSafetySettingsScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ModerationAndSafetySettingsScreenViewModelAction, Never> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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<ModerationAndSafetySettingsScreenViewModelAction, Never> { get }
|
||||
var context: ModerationAndSafetySettingsScreenViewModelType.Context { get }
|
||||
}
|
||||
+113
@@ -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)]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user