mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Security and privacy part 1 (#3617)
* added the security and settings button in details * added content to the view * added enable encryption alert * updated preview tests and the UI * removed wrong plists committed by mistake * pr suggestions
This commit is contained in:
@@ -405,6 +405,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .mediaEventsTimeline(previousState: fromState)
|
||||
case (.mediaEventsTimeline(let previousState), .dismissMediaEventsTimeline):
|
||||
return previousState
|
||||
|
||||
case (.roomDetails, .presentSecurityAndPrivacyScreen):
|
||||
return .securityAndPrivacy(previousState: fromState)
|
||||
case (.securityAndPrivacy(let previousState), .dismissSecurityAndPrivacyScreen):
|
||||
return previousState
|
||||
|
||||
default:
|
||||
return nil
|
||||
@@ -571,6 +576,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
Task { await self.startMediaEventsTimelineFlow() }
|
||||
case (.mediaEventsTimeline, .dismissMediaEventsTimeline, .roomDetails):
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentSecurityAndPrivacyScreen, .securityAndPrivacy):
|
||||
presentSecurityAndPrivacyScreen()
|
||||
case (.securityAndPrivacy, .dismissSecurityAndPrivacyScreen, .roomDetails):
|
||||
break
|
||||
|
||||
// Child flow
|
||||
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
||||
@@ -849,6 +859,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
||||
case .presentMediaEventsTimeline:
|
||||
stateMachine.tryEvent(.presentMediaEventsTimeline)
|
||||
case .presentSecurityAndPrivacyScreen:
|
||||
stateMachine.tryEvent(.presentSecurityAndPrivacyScreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -1453,6 +1465,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentSecurityAndPrivacyScreen() {
|
||||
let coordinator = SecurityAndPrivacyScreenCoordinator(parameters: .init(roomProxy: roomProxy))
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .done:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissSecurityAndPrivacyScreen)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Other flows
|
||||
|
||||
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
||||
@@ -1604,6 +1634,7 @@ private extension RoomFlowCoordinator {
|
||||
case resolveSendFailure
|
||||
case knockRequestsList(previousState: State)
|
||||
case mediaEventsTimeline(previousState: State)
|
||||
case securityAndPrivacy(previousState: State)
|
||||
|
||||
/// A child flow is in progress.
|
||||
case presentingChild(childRoomID: String, previousState: State)
|
||||
@@ -1687,6 +1718,9 @@ private extension RoomFlowCoordinator {
|
||||
|
||||
case presentMediaEventsTimeline
|
||||
case dismissMediaEventsTimeline
|
||||
|
||||
case presentSecurityAndPrivacyScreen
|
||||
case dismissSecurityAndPrivacyScreen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ enum RoomDetailsScreenCoordinatorAction {
|
||||
case presentPinnedEventsTimeline
|
||||
case presentMediaEventsTimeline
|
||||
case presentKnockingRequestsListScreen
|
||||
case presentSecurityAndPrivacyScreen
|
||||
}
|
||||
|
||||
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -85,6 +86,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentMediaEventsTimeline)
|
||||
case .displayKnockingRequests:
|
||||
actionsSubject.send(.presentKnockingRequestsListScreen)
|
||||
case .displaySecurityAndPrivacy:
|
||||
actionsSubject.send(.presentSecurityAndPrivacyScreen)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -24,6 +24,7 @@ enum RoomDetailsScreenViewModelAction {
|
||||
case displayPinnedEventsTimeline
|
||||
case displayMediaEventsTimeline
|
||||
case displayKnockingRequests
|
||||
case displaySecurityAndPrivacy
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
@@ -56,6 +57,10 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers)
|
||||
}
|
||||
|
||||
var canSeeSecurityAndPrivacy: Bool {
|
||||
knockingEnabled && dmRecipient == nil && canEditRolesOrPermissions
|
||||
}
|
||||
|
||||
var mediaBrowserEnabled = false
|
||||
|
||||
var canEdit: Bool {
|
||||
@@ -198,6 +203,7 @@ enum RoomDetailsScreenViewAction {
|
||||
case processTapPolls
|
||||
case toggleFavourite(isFavourite: Bool)
|
||||
case processTapRolesAndPermissions
|
||||
case processTapSecurityAndPrivacy
|
||||
case processTapCall
|
||||
case processTapPinnedEvents
|
||||
case processTapMediaEvents
|
||||
|
||||
@@ -172,6 +172,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
actionsSubject.send(.displayMediaEventsTimeline)
|
||||
case .processTapRequestsToJoin:
|
||||
actionsSubject.send(.displayKnockingRequests)
|
||||
case .processTapSecurityAndPrivacy:
|
||||
actionsSubject.send(.displaySecurityAndPrivacy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,14 @@ struct RoomDetailsScreen: View {
|
||||
.onChange(of: context.isFavourite) { _, newValue in
|
||||
context.send(viewAction: .toggleFavourite(isFavourite: newValue))
|
||||
}
|
||||
|
||||
if context.viewState.canSeeSecurityAndPrivacy {
|
||||
ListRow(label: .default(title: L10n.screenRoomDetailsSecurityAndPrivacyTitle,
|
||||
icon: \.lock),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .processTapSecurityAndPrivacy)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct SecurityAndPrivacyScreenCoordinatorParameters {
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyScreenCoordinatorAction {
|
||||
case done
|
||||
|
||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||
}
|
||||
|
||||
final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SecurityAndPrivacyScreenCoordinatorParameters
|
||||
private let viewModel: SecurityAndPrivacyScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SecurityAndPrivacyScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: SecurityAndPrivacyScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: parameters.roomProxy)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .done:
|
||||
actionsSubject.send(.done)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SecurityAndPrivacyScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SecurityAndPrivacyScreenViewModelAction {
|
||||
case done
|
||||
}
|
||||
|
||||
struct SecurityAndPrivacyScreenViewState: BindableState {
|
||||
var bindings: SecurityAndPrivacyScreenViewStateBindings
|
||||
|
||||
var currentSettings: SecurityAndPrivacySettings
|
||||
|
||||
var hasChanges: Bool {
|
||||
currentSettings != bindings.desiredSettings
|
||||
}
|
||||
|
||||
init(accessType: SecurityAndPrivacyRoomAccessType,
|
||||
isEncryptionEnabled: Bool) {
|
||||
let settings = SecurityAndPrivacySettings(accessType: accessType, isEncryptionEnabled: isEncryptionEnabled)
|
||||
currentSettings = settings
|
||||
bindings = SecurityAndPrivacyScreenViewStateBindings(desiredSettings: settings)
|
||||
}
|
||||
}
|
||||
|
||||
struct SecurityAndPrivacyScreenViewStateBindings {
|
||||
var desiredSettings: SecurityAndPrivacySettings
|
||||
var alertInfo: AlertInfo<SecurityAndPrivacyAlertType>?
|
||||
}
|
||||
|
||||
struct SecurityAndPrivacySettings: Equatable {
|
||||
var accessType: SecurityAndPrivacyRoomAccessType
|
||||
var isEncryptionEnabled: Bool
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyRoomAccessType {
|
||||
case inviteOnly
|
||||
case askToJoin
|
||||
case anyone
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyAlertType {
|
||||
case enableEncryption
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyScreenViewAction {
|
||||
case save
|
||||
case tryUpdatingEncryption(Bool)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias SecurityAndPrivacyScreenViewModelType = StateStoreViewModel<SecurityAndPrivacyScreenViewState, SecurityAndPrivacyScreenViewAction>
|
||||
|
||||
class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, SecurityAndPrivacyScreenViewModelProtocol {
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SecurityAndPrivacyScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
super.init(initialViewState: SecurityAndPrivacyScreenViewState(accessType: roomProxy.infoPublisher.value.roomAccessType,
|
||||
isEncryptionEnabled: roomProxy.isEncrypted))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: SecurityAndPrivacyScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .save:
|
||||
actionsSubject.send(.done)
|
||||
case .tryUpdatingEncryption(let updatedValue):
|
||||
if updatedValue {
|
||||
state.bindings.alertInfo = .init(id: .enableEncryption,
|
||||
title: L10n.screenSecurityAndPrivacyEnableEncryptionAlertTitle,
|
||||
message: L10n.screenSecurityAndPrivacyEnableEncryptionAlertDescription,
|
||||
primaryButton: .init(title: L10n.screenSecurityAndPrivacyEnableEncryptionAlertConfirmButtonTitle,
|
||||
action: { [weak self] in self?.state.bindings.desiredSettings.isEncryptionEnabled = true }),
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
} else {
|
||||
state.bindings.desiredSettings.isEncryptionEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomInfoProxy {
|
||||
var roomAccessType: SecurityAndPrivacyRoomAccessType {
|
||||
switch joinRule {
|
||||
case .invite, .restricted:
|
||||
return .inviteOnly
|
||||
case .knock, .knockRestricted:
|
||||
return .askToJoin
|
||||
default:
|
||||
return .anyone
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol SecurityAndPrivacyScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenViewModelAction, Never> { get }
|
||||
var context: SecurityAndPrivacyScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct SecurityAndPrivacyScreen: View {
|
||||
@ObservedObject var context: SecurityAndPrivacyScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
roomAccessSection
|
||||
encryptionSection
|
||||
}
|
||||
.compoundList()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(L10n.screenSecurityAndPrivacyTitle)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
private var roomAccessSection: some View {
|
||||
Section {
|
||||
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle,
|
||||
description: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription),
|
||||
kind: .selection(isSelected: context.desiredSettings.accessType == .inviteOnly) { context.desiredSettings.accessType = .inviteOnly })
|
||||
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyAskToJoinOptionTitle,
|
||||
description: L10n.screenSecurityAndPrivacyAskToJoinOptionDescription),
|
||||
kind: .selection(isSelected: context.desiredSettings.accessType == .askToJoin) { context.desiredSettings.accessType = .askToJoin })
|
||||
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle,
|
||||
description: L10n.screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription),
|
||||
kind: .selection(isSelected: context.desiredSettings.accessType == .anyone) { context.desiredSettings.accessType = .anyone })
|
||||
} header: {
|
||||
Text(L10n.screenSecurityAndPrivacyRoomAccessSectionTitle)
|
||||
.compoundListSectionHeader()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var encryptionSection: some View {
|
||||
let encryptionBinding = Binding<Bool>(get: {
|
||||
context.desiredSettings.isEncryptionEnabled
|
||||
}, set: { newValue in
|
||||
context.send(viewAction: .tryUpdatingEncryption(newValue))
|
||||
})
|
||||
|
||||
Section {
|
||||
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyEncryptionToggleTitle),
|
||||
kind: .toggle(encryptionBinding))
|
||||
// We don't allow editing the encryption state if the current setting on the server is `enabled`
|
||||
.disabled(context.viewState.currentSettings.isEncryptionEnabled)
|
||||
} header: {
|
||||
Text(L10n.screenSecurityAndPrivacyEncryptionSectionTitle)
|
||||
.compoundListSectionHeader()
|
||||
} footer: {
|
||||
Text(L10n.screenSecurityAndPrivacyEncryptionSectionFooter)
|
||||
.compoundListSectionFooter()
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.actionSave) {
|
||||
context.send(viewAction: .save)
|
||||
}
|
||||
.disabled(!context.viewState.hasChanges)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
// TODO: Add back TestablePreview, this is WIP so running preview tests for it is not necessary
|
||||
struct SecurityAndPrivacyScreen_Previews: PreviewProvider {
|
||||
static let inviteOnlyViewModel = SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(joinRule: .invite)))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
SecurityAndPrivacyScreen(context: inviteOnlyViewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user