single space access implementation

also added the copies for the single and multiple spaces selection
This commit is contained in:
Mauro Romito
2025-11-25 17:55:23 +01:00
committed by Mauro
parent 5d2efb7f76
commit 997353fdec
10 changed files with 337 additions and 41 deletions
@@ -648,11 +648,15 @@
"screen_security_and_privacy_encryption_section_footer" = "Once enabled, encryption cannot be disabled."; "screen_security_and_privacy_encryption_section_footer" = "Once enabled, encryption cannot be disabled.";
"screen_security_and_privacy_encryption_toggle_title" = "Enable end-to-end encryption"; "screen_security_and_privacy_encryption_toggle_title" = "Enable end-to-end encryption";
"screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join."; "screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join.";
"screen_security_and_privacy_room_access_footer" = "Choose which spaces’ members can join this room without an invitation. %1$@";
"screen_security_and_privacy_room_access_footer_manage_spaces_action" = "Manage spaces";
"screen_security_and_privacy_room_access_invite_only_option_description" = "Only invited people can join."; "screen_security_and_privacy_room_access_invite_only_option_description" = "Only invited people can join.";
"screen_security_and_privacy_room_access_invite_only_option_title" = "Invite only"; "screen_security_and_privacy_room_access_invite_only_option_title" = "Invite only";
"screen_security_and_privacy_room_access_section_header" = "Access"; "screen_security_and_privacy_room_access_section_header" = "Access";
"screen_security_and_privacy_room_access_space_members_option_description" = "Spaces are not currently supported"; "screen_security_and_privacy_room_access_space_members_option_multiple_parents_description" = "Anyone in authorized spaces can join.";
"screen_security_and_privacy_room_access_space_members_option_single_parent_description" = "Anyone in %1$@ can join.";
"screen_security_and_privacy_room_access_space_members_option_title" = "Space members"; "screen_security_and_privacy_room_access_space_members_option_title" = "Space members";
"screen_security_and_privacy_room_access_space_members_option_unavailable_description" = "Spaces are not currently supported";
"screen_security_and_privacy_room_address_section_header" = "Address"; "screen_security_and_privacy_room_address_section_header" = "Address";
"screen_security_and_privacy_room_directory_visibility_section_footer" = "Allow for this room to be found by searching %1$@ public room directory"; "screen_security_and_privacy_room_directory_visibility_section_footer" = "Allow for this room to be found by searching %1$@ public room directory";
"screen_security_and_privacy_room_directory_visibility_toggle_description" = "Allow to be found by searching the public directory."; "screen_security_and_privacy_room_directory_visibility_toggle_description" = "Allow to be found by searching the public directory.";
@@ -648,11 +648,15 @@
"screen_security_and_privacy_encryption_section_footer" = "Once enabled, encryption cannot be disabled."; "screen_security_and_privacy_encryption_section_footer" = "Once enabled, encryption cannot be disabled.";
"screen_security_and_privacy_encryption_toggle_title" = "Enable end-to-end encryption"; "screen_security_and_privacy_encryption_toggle_title" = "Enable end-to-end encryption";
"screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join."; "screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join.";
"screen_security_and_privacy_room_access_footer" = "Choose which spaces’ members can join this room without an invitation. %1$@";
"screen_security_and_privacy_room_access_footer_manage_spaces_action" = "Manage spaces";
"screen_security_and_privacy_room_access_invite_only_option_description" = "Only invited people can join."; "screen_security_and_privacy_room_access_invite_only_option_description" = "Only invited people can join.";
"screen_security_and_privacy_room_access_invite_only_option_title" = "Invite only"; "screen_security_and_privacy_room_access_invite_only_option_title" = "Invite only";
"screen_security_and_privacy_room_access_section_header" = "Access"; "screen_security_and_privacy_room_access_section_header" = "Access";
"screen_security_and_privacy_room_access_space_members_option_description" = "Spaces are not currently supported"; "screen_security_and_privacy_room_access_space_members_option_multiple_parents_description" = "Anyone in authorized spaces can join.";
"screen_security_and_privacy_room_access_space_members_option_single_parent_description" = "Anyone in %1$@ can join.";
"screen_security_and_privacy_room_access_space_members_option_title" = "Space members"; "screen_security_and_privacy_room_access_space_members_option_title" = "Space members";
"screen_security_and_privacy_room_access_space_members_option_unavailable_description" = "Spaces are not currently supported";
"screen_security_and_privacy_room_address_section_header" = "Address"; "screen_security_and_privacy_room_address_section_header" = "Address";
"screen_security_and_privacy_room_directory_visibility_section_footer" = "Allow for this room to be found by searching %1$@ public room directory"; "screen_security_and_privacy_room_directory_visibility_section_footer" = "Allow for this room to be found by searching %1$@ public room directory";
"screen_security_and_privacy_room_directory_visibility_toggle_description" = "Allow to be found by searching the public directory."; "screen_security_and_privacy_room_directory_visibility_toggle_description" = "Allow to be found by searching the public directory.";
+14 -2
View File
@@ -2768,16 +2768,28 @@ internal enum L10n {
internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_description") } internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_description") }
/// Anyone /// Anyone
internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_title") } internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_title") }
/// Choose which spaces’ members can join this room without an invitation. %1$@
internal static func screenSecurityAndPrivacyRoomAccessFooter(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_security_and_privacy_room_access_footer", String(describing: p1))
}
/// Manage spaces
internal static var screenSecurityAndPrivacyRoomAccessFooterManageSpacesAction: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_footer_manage_spaces_action") }
/// Only invited people can join. /// Only invited people can join.
internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_description") } internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_description") }
/// Invite only /// Invite only
internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_title") } internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_title") }
/// Access /// Access
internal static var screenSecurityAndPrivacyRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_section_header") } internal static var screenSecurityAndPrivacyRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_section_header") }
/// Spaces are not currently supported /// Anyone in authorized spaces can join.
internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_description") } internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionMultipleParentsDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_multiple_parents_description") }
/// Anyone in %1$@ can join.
internal static func screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_single_parent_description", String(describing: p1))
}
/// Space members /// Space members
internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_title") } internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_title") }
/// Spaces are not currently supported
internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionUnavailableDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_unavailable_description") }
/// You’ll need an address in order to make it visible in the public directory. /// You’ll need an address in order to make it visible in the public directory.
internal static var screenSecurityAndPrivacyRoomAddressSectionFooter: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_address_section_footer") } internal static var screenSecurityAndPrivacyRoomAddressSectionFooter: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_address_section_footer") }
/// Address /// Address
@@ -16616,6 +16616,76 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
return leaveSpaceSpaceIDReturnValue return leaveSpaceSpaceIDReturnValue
} }
} }
//MARK: - joinedParents
var joinedParentsRoomIDUnderlyingCallsCount = 0
var joinedParentsRoomIDCallsCount: Int {
get {
if Thread.isMainThread {
return joinedParentsRoomIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = joinedParentsRoomIDUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
joinedParentsRoomIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
joinedParentsRoomIDUnderlyingCallsCount = newValue
}
}
}
}
var joinedParentsRoomIDCalled: Bool {
return joinedParentsRoomIDCallsCount > 0
}
var joinedParentsRoomIDReceivedRoomID: String?
var joinedParentsRoomIDReceivedInvocations: [String] = []
var joinedParentsRoomIDUnderlyingReturnValue: Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>!
var joinedParentsRoomIDReturnValue: Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>! {
get {
if Thread.isMainThread {
return joinedParentsRoomIDUnderlyingReturnValue
} else {
var returnValue: Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>? = nil
DispatchQueue.main.sync {
returnValue = joinedParentsRoomIDUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
joinedParentsRoomIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
joinedParentsRoomIDUnderlyingReturnValue = newValue
}
}
}
}
var joinedParentsRoomIDClosure: ((String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>)?
func joinedParents(roomID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError> {
joinedParentsRoomIDCallsCount += 1
joinedParentsRoomIDReceivedRoomID = roomID
DispatchQueue.main.async {
self.joinedParentsRoomIDReceivedInvocations.append(roomID)
}
if let joinedParentsRoomIDClosure = joinedParentsRoomIDClosure {
return await joinedParentsRoomIDClosure(roomID)
} else {
return joinedParentsRoomIDReturnValue
}
}
} }
class StaticRoomSummaryProviderMock: StaticRoomSummaryProviderProtocol, @unchecked Sendable { class StaticRoomSummaryProviderMock: StaticRoomSummaryProviderProtocol, @unchecked Sendable {
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> { var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> {
@@ -13,6 +13,7 @@ import MatrixRustSDK
extension SpaceServiceProxyMock { extension SpaceServiceProxyMock {
struct Configuration { struct Configuration {
var joinedSpaces: [SpaceRoomProxyProtocol] = [] var joinedSpaces: [SpaceRoomProxyProtocol] = []
var joinedParentSpaces: [SpaceRoomProxyProtocol] = []
var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:] var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:]
var leaveSpaceRooms: [LeaveSpaceRoom] = [] var leaveSpaceRooms: [LeaveSpaceRoom] = []
} }
@@ -21,6 +22,7 @@ extension SpaceServiceProxyMock {
self.init() self.init()
joinedSpacesPublisher = .init(configuration.joinedSpaces) joinedSpacesPublisher = .init(configuration.joinedSpaces)
joinedParentsRoomIDReturnValue = .success(configuration.joinedParentSpaces)
spaceRoomListSpaceIDClosure = { spaceID in spaceRoomListSpaceIDClosure = { spaceID in
if let spaceRoomList = configuration.spaceRoomLists[spaceID] { if let spaceRoomList = configuration.spaceRoomLists[spaceID] {
.success(spaceRoomList) .success(spaceRoomList)
@@ -7,23 +7,45 @@
// //
import Foundation import Foundation
import MatrixRustSDK
enum SecurityAndPrivacyScreenViewModelAction { enum SecurityAndPrivacyScreenViewModelAction {
case displayEditAddressScreen case displayEditAddressScreen
} }
struct SecurityAndPrivacyScreenViewState: BindableState { 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 let serverName: String
var currentSettings: SecurityAndPrivacySettings var currentSettings: SecurityAndPrivacySettings
var bindings: SecurityAndPrivacyScreenViewStateBindings var bindings: SecurityAndPrivacyScreenViewStateBindings
var canonicalAlias: String? var canonicalAlias: String?
var isKnockingEnabled: Bool var isKnockingEnabled: Bool
var isSpaceSettingsEnabled: Bool
var isSpace: Bool var isSpace: Bool
var canEditAddress = false var canEditAddress = false
var canEditJoinRule = false var canEditJoinRule = false
var canEnableEncryption = false var canEnableEncryption = false
var canEditHistoryVisibility = 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 { private var hasChanges: Bool {
currentSettings != bindings.desiredSettings currentSettings != bindings.desiredSettings
@@ -45,16 +67,77 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
} }
return options 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, init(serverName: String,
accessType: SecurityAndPrivacyRoomAccessType, accessType: SecurityAndPrivacyRoomAccessType,
isEncryptionEnabled: Bool, isEncryptionEnabled: Bool,
historyVisibility: SecurityAndPrivacyHistoryVisibility, historyVisibility: SecurityAndPrivacyHistoryVisibility,
isSpace: Bool, isSpace: Bool,
isKnockingEnabled: Bool) { isKnockingEnabled: Bool,
isSpaceSettingsEnabled: Bool) {
self.serverName = serverName self.serverName = serverName
self.isKnockingEnabled = isKnockingEnabled self.isKnockingEnabled = isKnockingEnabled
self.isSpace = isSpace self.isSpace = isSpace
self.isSpaceSettingsEnabled = isSpaceSettingsEnabled
let settings = SecurityAndPrivacySettings(accessType: accessType, let settings = SecurityAndPrivacySettings(accessType: accessType,
isEncryptionEnabled: isEncryptionEnabled, isEncryptionEnabled: isEncryptionEnabled,
@@ -76,11 +159,39 @@ struct SecurityAndPrivacySettings: Equatable {
var isVisibileInRoomDirectory: Bool? var isVisibileInRoomDirectory: Bool?
} }
enum SecurityAndPrivacyRoomAccessType { enum SecurityAndPrivacyRoomAccessType: Equatable {
case inviteOnly case inviteOnly
case askToJoin case askToJoin
case askToJoinWithSpaceUsers(spaceIDs: [String])
case anyone 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 { enum SecurityAndPrivacyAlertType {
@@ -91,6 +202,8 @@ enum SecurityAndPrivacyScreenViewAction {
case save case save
case tryUpdatingEncryption(Bool) case tryUpdatingEncryption(Bool)
case editAddress case editAddress
case selectedSpaceMembersAccess
case manageSpaces
} }
enum SecurityAndPrivacyHistoryVisibility { enum SecurityAndPrivacyHistoryVisibility {
@@ -37,7 +37,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted, isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted,
historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility, historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility,
isSpace: roomProxy.infoPublisher.value.isSpace, isSpace: roomProxy.infoPublisher.value.isSpace,
isKnockingEnabled: appSettings.knockingEnabled)) isKnockingEnabled: appSettings.knockingEnabled,
isSpaceSettingsEnabled: appSettings.spaceSettingsEnabled))
if let powerLevels = roomProxy.infoPublisher.value.powerLevels { if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
setupPermissions(powerLevels: powerLevels) setupPermissions(powerLevels: powerLevels)
@@ -45,6 +46,14 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
setupRoomDirectoryVisibility() setupRoomDirectoryVisibility()
setupSubscriptions() setupSubscriptions()
Task {
switch await clientProxy.spaceService.joinedParents(roomID: roomProxy.id) {
case .success(let joinedParentSpaces):
state.joinedParentSpaces = joinedParentSpaces
case .failure:
break
}
}
} }
// MARK: - Public // MARK: - Public
@@ -69,6 +78,11 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
} }
case .editAddress: case .editAddress:
actionsSubject.send(.displayEditAddressScreen) actionsSubject.send(.displayEditAddressScreen)
case .selectedSpaceMembersAccess:
handleSelectedSpaceMembersAccess()
case .manageSpaces:
// TODO: Implement multiple space selection
break
} }
} }
@@ -127,6 +141,10 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
appSettings.$knockingEnabled appSettings.$knockingEnabled
.weakAssign(to: \.state.isKnockingEnabled, on: self) .weakAssign(to: \.state.isKnockingEnabled, on: self)
.store(in: &cancellables) .store(in: &cancellables)
appSettings.$spaceSettingsEnabled
.weakAssign(to: \.state.isSpaceSettingsEnabled, on: self)
.store(in: &cancellables)
} }
private func setupPermissions(powerLevels: RoomPowerLevelsProxyProtocol) { 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 static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading"
private func showLoadingIndicator() { private func showLoadingIndicator() {
@@ -225,25 +255,10 @@ private extension SecurityAndPrivacyRoomAccessType {
.knock .knock
case .anyone: case .anyone:
.public .public
case .spaceUsers: case .spaceUsers(let spaceIDs):
fatalError("The user shouldn't be able to select this rule") .restricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
} case .askToJoinWithSpaceUsers(let spaceIDs):
} .knockRestricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
}
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
} }
} }
} }
@@ -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
}
}
}
@@ -18,7 +18,7 @@ struct SecurityAndPrivacyScreen: View {
roomAccessSection roomAccessSection
} }
if context.desiredSettings.accessType != .inviteOnly, context.viewState.canEditAddress { if context.desiredSettings.accessType.isAddressRequired, context.viewState.canEditAddress {
visibilitySection visibilitySection
if let canonicalAlias = context.viewState.canonicalAlias { if let canonicalAlias = context.viewState.canonicalAlias {
addressSection(canonicalAlias: canonicalAlias) addressSection(canonicalAlias: canonicalAlias)
@@ -57,13 +57,14 @@ struct SecurityAndPrivacyScreen: View {
.disabled(!context.viewState.isKnockingEnabled) .disabled(!context.viewState.isKnockingEnabled)
} }
if context.viewState.currentSettings.accessType == .spaceUsers { if context.viewState.isSpaceMembersOptionAvailable {
ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionTitle, ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionTitle,
description: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionDescription, description: context.viewState.spaceMembersDescription,
icon: \.space), icon: \.space),
kind: .selection(isSelected: context.desiredSettings.accessType == .spaceUsers) { }) kind: .selection(isSelected: context.desiredSettings.accessType.isSpaceUsers) {
// This is not supported so it will always be disabled and has no handler context.send(viewAction: .selectedSpaceMembersAccess)
.disabled(true) })
.disabled(!context.viewState.isSpaceMembersOptionSelectable)
} }
ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle, ListRow(label: .default(title: L10n.screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle,
@@ -73,6 +74,15 @@ struct SecurityAndPrivacyScreen: View {
} header: { } header: {
Text(L10n.screenSecurityAndPrivacyRoomAccessSectionHeader) Text(L10n.screenSecurityAndPrivacyRoomAccessSectionHeader)
.compoundListSectionHeader() .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()) appSettings: AppSettings())
}() }()
static let restrictedViewModel = { static let singleSpaceMembersViewModel = {
AppSettings.resetAllSettings() AppSettings.resetAllSettings()
let appSettings = AppSettings()
appSettings.spaceSettingsEnabled = true
let space = [SpaceRoomProxyProtocol].mockSingleRoom[0]
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false, return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
canonicalAlias: "#room:matrix.org", canonicalAlias: "#room:matrix.org",
members: .allMembersAsCreator, members: .allMembersAsCreator,
joinRule: .restricted(rules: []), joinRule: .restricted(rules: [.roomMembership(roomId: space.id)]),
isVisibleInPublicDirectory: true)), 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(), userIndicatorController: UserIndicatorControllerMock(),
appSettings: AppSettings()) appSettings: AppSettings())
}() }()
@@ -278,17 +310,25 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
.previewDisplayName("Public room without address") .previewDisplayName("Public room without address")
NavigationStack { 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 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 { NavigationStack {
SecurityAndPrivacyScreen(context: askToJoinViewModel.context) SecurityAndPrivacyScreen(context: askToJoinViewModel.context)
} }
.snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in .snapshotPreferences(expect: askToJoinViewModel.context.$viewState.map { state in
state.currentSettings.isVisibileInRoomDirectory == true state.currentSettings.isVisibileInRoomDirectory == true
}) })
.previewDisplayName("Ask to join room") .previewDisplayName("Ask to join room")
@@ -296,7 +336,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack { NavigationStack {
SecurityAndPrivacyScreen(context: publicSpaceViewModel.context) SecurityAndPrivacyScreen(context: publicSpaceViewModel.context)
} }
.snapshotPreferences(expect: restrictedViewModel.context.$viewState.map { state in .snapshotPreferences(expect: publicSpaceViewModel.context.$viewState.map { state in
state.currentSettings.isVisibileInRoomDirectory == true state.currentSettings.isVisibileInRoomDirectory == true
}) })
.previewDisplayName("Public space") .previewDisplayName("Public space")
@@ -49,6 +49,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
} }
} }
func joinedParents(roomID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError> {
do {
return try await .success(spaceService.joinedParentsOfChild(childId: roomID).map(SpaceRoomProxy.init))
} catch {
MXLog.error("Failed to get joined parents for \(roomID): \(error)")
return .failure(.sdkError(error))
}
}
// MARK: - Private // MARK: - Private
private func handleUpdates(_ updates: [SpaceListUpdate]) { private func handleUpdates(_ updates: [SpaceListUpdate]) {
@@ -19,4 +19,6 @@ protocol SpaceServiceProxyProtocol {
func spaceRoomList(spaceID: String) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError> func spaceRoomList(spaceID: String) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
/// Returns all the parent spaces of a room that user has joined.
func joinedParents(roomID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>
} }