mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
single space access implementation
also added the copies for the single and multiple spaces selection
This commit is contained in:
+116
-3
@@ -7,23 +7,45 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum SecurityAndPrivacyScreenViewModelAction {
|
||||
case displayEditAddressScreen
|
||||
}
|
||||
|
||||
struct SecurityAndPrivacyScreenViewState: BindableState {
|
||||
private static let accessSectionFooterAttributedString = {
|
||||
let linkPlaceholder = "{link}"
|
||||
var footer = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder))
|
||||
var linkString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooterManageSpacesAction)
|
||||
// Doesn't really matter
|
||||
linkString.link = .init(stringLiteral: "action://manageSpace")
|
||||
linkString.bold()
|
||||
footer.replace(linkPlaceholder, with: linkString)
|
||||
return footer
|
||||
}()
|
||||
|
||||
let serverName: String
|
||||
var currentSettings: SecurityAndPrivacySettings
|
||||
var bindings: SecurityAndPrivacyScreenViewStateBindings
|
||||
var canonicalAlias: String?
|
||||
var isKnockingEnabled: Bool
|
||||
var isSpaceSettingsEnabled: Bool
|
||||
var isSpace: Bool
|
||||
|
||||
var canEditAddress = false
|
||||
var canEditJoinRule = false
|
||||
var canEnableEncryption = false
|
||||
var canEditHistoryVisibility = false
|
||||
var joinedParentSpaces: [SpaceRoomProxyProtocol] = []
|
||||
|
||||
var selectableSpacesCount: Int {
|
||||
Set(joinedParentSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count
|
||||
}
|
||||
|
||||
private var desiredJoinedParentSpaces: [SpaceRoomProxyProtocol] {
|
||||
joinedParentSpaces.filter { bindings.desiredSettings.accessType.spaceIDs.contains($0.id) }
|
||||
}
|
||||
|
||||
private var hasChanges: Bool {
|
||||
currentSettings != bindings.desiredSettings
|
||||
@@ -45,16 +67,77 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
var isSpaceMembersOptionAvailable: Bool {
|
||||
currentSettings.accessType.isSpaceUsers || isSpaceMembersOptionSelectable
|
||||
}
|
||||
|
||||
var isSpaceMembersOptionSelectable: Bool {
|
||||
isSpaceSettingsEnabled && selectableSpacesCount > 0
|
||||
}
|
||||
|
||||
var spaceMembersDescription: String {
|
||||
if isSpaceMembersOptionSelectable {
|
||||
switch spaceSelection {
|
||||
case .singleJoined(let joinedParentSpace):
|
||||
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedParentSpace.name)
|
||||
case .singleUnknown(let id):
|
||||
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(id)
|
||||
case .multiple:
|
||||
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionMultipleParentsDescription
|
||||
}
|
||||
} else {
|
||||
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionUnavailableDescription
|
||||
}
|
||||
}
|
||||
|
||||
var accessSectionFooter: AttributedString? {
|
||||
if bindings.desiredSettings.accessType.isSpaceUsers, isSpaceMembersOptionSelectable, selectableSpacesCount > 1 {
|
||||
Self.accessSectionFooterAttributedString
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
enum SpaceSelection {
|
||||
/// There is only one available parent space for selection and is joined by the user
|
||||
case singleJoined(SpaceRoomProxyProtocol)
|
||||
/// There is only one available space for selection and is unknown to the user
|
||||
case singleUnknown(id: String)
|
||||
/// Multiple spaces are available for selection
|
||||
case multiple
|
||||
}
|
||||
|
||||
var spaceSelection: SpaceSelection {
|
||||
if selectableSpacesCount > 1 {
|
||||
.multiple
|
||||
} else if let desiredJoinedParent = desiredJoinedParentSpaces.first {
|
||||
// The parent space is joined by the user and is also currently selected
|
||||
.singleJoined(desiredJoinedParent)
|
||||
} else if let joinedParent = joinedParentSpaces.first {
|
||||
// The parent space is joined by the user but is not currently selected
|
||||
.singleJoined(joinedParent)
|
||||
} else if let unknownSpaceID = bindings.desiredSettings.accessType.spaceIDs.first {
|
||||
// The space is not joined by the user but is currently selected
|
||||
.singleUnknown(id: unknownSpaceID)
|
||||
} else {
|
||||
// Not reachable because it would mean the selectable spaces are more than 1
|
||||
// but are neither selected and/or joined parents.
|
||||
fatalError("Not reachable")
|
||||
}
|
||||
}
|
||||
|
||||
init(serverName: String,
|
||||
accessType: SecurityAndPrivacyRoomAccessType,
|
||||
isEncryptionEnabled: Bool,
|
||||
historyVisibility: SecurityAndPrivacyHistoryVisibility,
|
||||
isSpace: Bool,
|
||||
isKnockingEnabled: Bool) {
|
||||
isKnockingEnabled: Bool,
|
||||
isSpaceSettingsEnabled: Bool) {
|
||||
self.serverName = serverName
|
||||
self.isKnockingEnabled = isKnockingEnabled
|
||||
self.isSpace = isSpace
|
||||
self.isSpaceSettingsEnabled = isSpaceSettingsEnabled
|
||||
|
||||
let settings = SecurityAndPrivacySettings(accessType: accessType,
|
||||
isEncryptionEnabled: isEncryptionEnabled,
|
||||
@@ -76,11 +159,39 @@ struct SecurityAndPrivacySettings: Equatable {
|
||||
var isVisibileInRoomDirectory: Bool?
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyRoomAccessType {
|
||||
enum SecurityAndPrivacyRoomAccessType: Equatable {
|
||||
case inviteOnly
|
||||
case askToJoin
|
||||
case askToJoinWithSpaceUsers(spaceIDs: [String])
|
||||
case anyone
|
||||
case spaceUsers
|
||||
case spaceUsers(spaceIDs: [String])
|
||||
|
||||
var isSpaceUsers: Bool {
|
||||
switch self {
|
||||
case .spaceUsers:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var isAddressRequired: Bool {
|
||||
switch self {
|
||||
case .inviteOnly, .spaceUsers:
|
||||
false
|
||||
case .anyone, .askToJoin, .askToJoinWithSpaceUsers:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var spaceIDs: [String] {
|
||||
switch self {
|
||||
case .spaceUsers(let spaceIDs), .askToJoinWithSpaceUsers(let spaceIDs):
|
||||
return spaceIDs
|
||||
case .inviteOnly, .askToJoin, .anyone:
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyAlertType {
|
||||
@@ -91,6 +202,8 @@ enum SecurityAndPrivacyScreenViewAction {
|
||||
case save
|
||||
case tryUpdatingEncryption(Bool)
|
||||
case editAddress
|
||||
case selectedSpaceMembersAccess
|
||||
case manageSpaces
|
||||
}
|
||||
|
||||
enum SecurityAndPrivacyHistoryVisibility {
|
||||
|
||||
+60
-20
@@ -37,7 +37,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
||||
isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted,
|
||||
historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility,
|
||||
isSpace: roomProxy.infoPublisher.value.isSpace,
|
||||
isKnockingEnabled: appSettings.knockingEnabled))
|
||||
isKnockingEnabled: appSettings.knockingEnabled,
|
||||
isSpaceSettingsEnabled: appSettings.spaceSettingsEnabled))
|
||||
|
||||
if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
|
||||
setupPermissions(powerLevels: powerLevels)
|
||||
@@ -45,6 +46,14 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
||||
|
||||
setupRoomDirectoryVisibility()
|
||||
setupSubscriptions()
|
||||
Task {
|
||||
switch await clientProxy.spaceService.joinedParents(roomID: roomProxy.id) {
|
||||
case .success(let joinedParentSpaces):
|
||||
state.joinedParentSpaces = joinedParentSpaces
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -69,6 +78,11 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
||||
}
|
||||
case .editAddress:
|
||||
actionsSubject.send(.displayEditAddressScreen)
|
||||
case .selectedSpaceMembersAccess:
|
||||
handleSelectedSpaceMembersAccess()
|
||||
case .manageSpaces:
|
||||
// TODO: Implement multiple space selection
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +141,10 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
||||
appSettings.$knockingEnabled
|
||||
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$spaceSettingsEnabled
|
||||
.weakAssign(to: \.state.isSpaceSettingsEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func setupPermissions(powerLevels: RoomPowerLevelsProxyProtocol) {
|
||||
@@ -202,6 +220,18 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSelectedSpaceMembersAccess() {
|
||||
switch context.viewState.spaceSelection {
|
||||
case .singleJoined(let joinedParent):
|
||||
context.desiredSettings.accessType = .spaceUsers(spaceIDs: [joinedParent.id])
|
||||
case .singleUnknown(let id):
|
||||
context.desiredSettings.accessType = .spaceUsers(spaceIDs: [id])
|
||||
case .multiple:
|
||||
// TODO: Implement multiple space selection
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
@@ -225,25 +255,10 @@ private extension SecurityAndPrivacyRoomAccessType {
|
||||
.knock
|
||||
case .anyone:
|
||||
.public
|
||||
case .spaceUsers:
|
||||
fatalError("The user shouldn't be able to select this rule")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Optional where Wrapped == JoinRule {
|
||||
var toSecurityAndPrivacyRoomAccessType: SecurityAndPrivacyRoomAccessType {
|
||||
switch self {
|
||||
case .none, .public:
|
||||
return .anyone
|
||||
case .invite:
|
||||
return .inviteOnly
|
||||
case .knock, .knockRestricted:
|
||||
return .askToJoin
|
||||
case .restricted:
|
||||
return .spaceUsers
|
||||
default:
|
||||
return .inviteOnly
|
||||
case .spaceUsers(let spaceIDs):
|
||||
.restricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
|
||||
case .askToJoinWithSpaceUsers(let spaceIDs):
|
||||
.knockRestricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,3 +288,28 @@ private extension SecurityAndPrivacyHistoryVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Optional where Wrapped == JoinRule {
|
||||
var toSecurityAndPrivacyRoomAccessType: SecurityAndPrivacyRoomAccessType {
|
||||
switch self {
|
||||
case .none, .public:
|
||||
return .anyone
|
||||
case .invite:
|
||||
return .inviteOnly
|
||||
case .knock, .knockRestricted:
|
||||
// TODO: Handle knock restricted with rules
|
||||
return .askToJoin
|
||||
case .restricted(let rules):
|
||||
let spaceIDs = rules.compactMap { rule in
|
||||
if case let .roomMembership(id) = rule {
|
||||
id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
return .spaceUsers(spaceIDs: spaceIDs)
|
||||
default:
|
||||
return .inviteOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-14
@@ -18,7 +18,7 @@ struct SecurityAndPrivacyScreen: View {
|
||||
roomAccessSection
|
||||
}
|
||||
|
||||
if context.desiredSettings.accessType != .inviteOnly, context.viewState.canEditAddress {
|
||||
if context.desiredSettings.accessType.isAddressRequired, context.viewState.canEditAddress {
|
||||
visibilitySection
|
||||
if let canonicalAlias = context.viewState.canonicalAlias {
|
||||
addressSection(canonicalAlias: canonicalAlias)
|
||||
@@ -57,13 +57,14 @@ struct SecurityAndPrivacyScreen: View {
|
||||
.disabled(!context.viewState.isKnockingEnabled)
|
||||
}
|
||||
|
||||
if context.viewState.currentSettings.accessType == .spaceUsers {
|
||||
if context.viewState.isSpaceMembersOptionAvailable {
|
||||
ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionTitle,
|
||||
description: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionDescription,
|
||||
description: context.viewState.spaceMembersDescription,
|
||||
icon: \.space),
|
||||
kind: .selection(isSelected: context.desiredSettings.accessType == .spaceUsers) { })
|
||||
// This is not supported so it will always be disabled and has no handler
|
||||
.disabled(true)
|
||||
kind: .selection(isSelected: context.desiredSettings.accessType.isSpaceUsers) {
|
||||
context.send(viewAction: .selectedSpaceMembersAccess)
|
||||
})
|
||||
.disabled(!context.viewState.isSpaceMembersOptionSelectable)
|
||||
}
|
||||
|
||||
ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle,
|
||||
@@ -73,6 +74,15 @@ struct SecurityAndPrivacyScreen: View {
|
||||
} header: {
|
||||
Text(L10n.screenSecurityAndPrivacyRoomAccessSectionHeader)
|
||||
.compoundListSectionHeader()
|
||||
} footer: {
|
||||
if let footer = context.viewState.accessSectionFooter {
|
||||
Text(footer)
|
||||
.compoundListSectionFooter()
|
||||
.environment(\.openURL, OpenURLAction { _ in
|
||||
context.send(viewAction: .manageSpaces)
|
||||
return .handled
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,14 +228,36 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: AppSettings())
|
||||
}()
|
||||
|
||||
static let restrictedViewModel = {
|
||||
static let singleSpaceMembersViewModel = {
|
||||
AppSettings.resetAllSettings()
|
||||
let appSettings = AppSettings()
|
||||
appSettings.spaceSettingsEnabled = true
|
||||
let space = [SpaceRoomProxyProtocol].mockSingleRoom[0]
|
||||
|
||||
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
|
||||
canonicalAlias: "#room:matrix.org",
|
||||
members: .allMembersAsCreator,
|
||||
joinRule: .restricted(rules: []),
|
||||
joinRule: .restricted(rules: [.roomMembership(roomId: space.id)]),
|
||||
isVisibleInPublicDirectory: true)),
|
||||
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org")),
|
||||
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
|
||||
spaceServiceConfiguration: .init(joinedParentSpaces: [space]))),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: AppSettings())
|
||||
}()
|
||||
|
||||
static let multipleSpacesMembersViewModel = {
|
||||
AppSettings.resetAllSettings()
|
||||
let appSettings = AppSettings()
|
||||
appSettings.spaceSettingsEnabled = true
|
||||
let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces
|
||||
|
||||
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
|
||||
canonicalAlias: "#room:matrix.org",
|
||||
members: .allMembersAsCreator,
|
||||
joinRule: .restricted(rules: spaces.map { .roomMembership(roomId: $0.id) }),
|
||||
isVisibleInPublicDirectory: true)),
|
||||
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
|
||||
spaceServiceConfiguration: .init(joinedParentSpaces: spaces))),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: AppSettings())
|
||||
}()
|
||||
@@ -278,17 +310,25 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
|
||||
.previewDisplayName("Public room without address")
|
||||
|
||||
NavigationStack {
|
||||
SecurityAndPrivacyScreen(context: restrictedViewModel.context)
|
||||
SecurityAndPrivacyScreen(context: singleSpaceMembersViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in
|
||||
.snapshotPreferences(expect: singleSpaceMembersViewModel.context.$viewState.map { state in
|
||||
state.currentSettings.isVisibileInRoomDirectory == true
|
||||
})
|
||||
.previewDisplayName("Restricted room")
|
||||
.previewDisplayName("Space members")
|
||||
|
||||
NavigationStack {
|
||||
SecurityAndPrivacyScreen(context: multipleSpacesMembersViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: multipleSpacesMembersViewModel.context.$viewState.map { state in
|
||||
state.currentSettings.isVisibileInRoomDirectory == true
|
||||
})
|
||||
.previewDisplayName("Multiple Spaces members")
|
||||
|
||||
NavigationStack {
|
||||
SecurityAndPrivacyScreen(context: askToJoinViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in
|
||||
.snapshotPreferences(expect: askToJoinViewModel.context.$viewState.map { state in
|
||||
state.currentSettings.isVisibileInRoomDirectory == true
|
||||
})
|
||||
.previewDisplayName("Ask to join room")
|
||||
@@ -296,7 +336,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
|
||||
NavigationStack {
|
||||
SecurityAndPrivacyScreen(context: publicSpaceViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in
|
||||
.snapshotPreferences(expect: publicSpaceViewModel.context.$viewState.map { state in
|
||||
state.currentSettings.isVisibileInRoomDirectory == true
|
||||
})
|
||||
.previewDisplayName("Public space")
|
||||
|
||||
Reference in New Issue
Block a user