handle the case where a non parent joined space is already available in the existing restricted join rule

This commit is contained in:
Mauro Romito
2025-12-11 16:53:13 +01:00
committed by Mauro
parent 00b2895e25
commit 55e88245f5
13 changed files with 266 additions and 42 deletions
+1 -1
View File
@@ -9570,7 +9570,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 25.12.10; version = 25.12.11;
}; };
}; };
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
@@ -158,8 +158,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "ae9a08f7faebb5cd2af7fe29860378009066a27e", "revision" : "8efbdc808f699f6df830f552a2b302e91d45431a",
"version" : "25.12.10" "version" : "25.12.11"
} }
}, },
{ {
@@ -16568,6 +16568,76 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
return spaceRoomListSpaceIDReturnValue return spaceRoomListSpaceIDReturnValue
} }
} }
//MARK: - spaceForIdentifier
var spaceForIdentifierSpaceIDUnderlyingCallsCount = 0
var spaceForIdentifierSpaceIDCallsCount: Int {
get {
if Thread.isMainThread {
return spaceForIdentifierSpaceIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = spaceForIdentifierSpaceIDUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
spaceForIdentifierSpaceIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
spaceForIdentifierSpaceIDUnderlyingCallsCount = newValue
}
}
}
}
var spaceForIdentifierSpaceIDCalled: Bool {
return spaceForIdentifierSpaceIDCallsCount > 0
}
var spaceForIdentifierSpaceIDReceivedSpaceID: String?
var spaceForIdentifierSpaceIDReceivedInvocations: [String] = []
var spaceForIdentifierSpaceIDUnderlyingReturnValue: Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError>!
var spaceForIdentifierSpaceIDReturnValue: Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError>! {
get {
if Thread.isMainThread {
return spaceForIdentifierSpaceIDUnderlyingReturnValue
} else {
var returnValue: Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError>? = nil
DispatchQueue.main.sync {
returnValue = spaceForIdentifierSpaceIDUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
spaceForIdentifierSpaceIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
spaceForIdentifierSpaceIDUnderlyingReturnValue = newValue
}
}
}
}
var spaceForIdentifierSpaceIDClosure: ((String) async -> Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError>)?
func spaceForIdentifier(spaceID: String) async -> Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError> {
spaceForIdentifierSpaceIDCallsCount += 1
spaceForIdentifierSpaceIDReceivedSpaceID = spaceID
DispatchQueue.main.async {
self.spaceForIdentifierSpaceIDReceivedInvocations.append(spaceID)
}
if let spaceForIdentifierSpaceIDClosure = spaceForIdentifierSpaceIDClosure {
return await spaceForIdentifierSpaceIDClosure(spaceID)
} else {
return spaceForIdentifierSpaceIDReturnValue
}
}
//MARK: - leaveSpace //MARK: - leaveSpace
var leaveSpaceSpaceIDUnderlyingCallsCount = 0 var leaveSpaceSpaceIDUnderlyingCallsCount = 0
@@ -23265,6 +23265,81 @@ open class SpaceServiceSDKMock: MatrixRustSDK.SpaceService, @unchecked Sendable
} }
} }
//MARK: - getSpaceRoom
open var getSpaceRoomRoomIdThrowableError: Error?
var getSpaceRoomRoomIdUnderlyingCallsCount = 0
open var getSpaceRoomRoomIdCallsCount: Int {
get {
if Thread.isMainThread {
return getSpaceRoomRoomIdUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = getSpaceRoomRoomIdUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
getSpaceRoomRoomIdUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
getSpaceRoomRoomIdUnderlyingCallsCount = newValue
}
}
}
}
open var getSpaceRoomRoomIdCalled: Bool {
return getSpaceRoomRoomIdCallsCount > 0
}
open var getSpaceRoomRoomIdReceivedRoomId: String?
open var getSpaceRoomRoomIdReceivedInvocations: [String] = []
var getSpaceRoomRoomIdUnderlyingReturnValue: SpaceRoom?
open var getSpaceRoomRoomIdReturnValue: SpaceRoom? {
get {
if Thread.isMainThread {
return getSpaceRoomRoomIdUnderlyingReturnValue
} else {
var returnValue: SpaceRoom?? = nil
DispatchQueue.main.sync {
returnValue = getSpaceRoomRoomIdUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
getSpaceRoomRoomIdUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
getSpaceRoomRoomIdUnderlyingReturnValue = newValue
}
}
}
}
open var getSpaceRoomRoomIdClosure: ((String) async throws -> SpaceRoom?)?
open override func getSpaceRoom(roomId: String) async throws -> SpaceRoom? {
if let error = getSpaceRoomRoomIdThrowableError {
throw error
}
getSpaceRoomRoomIdCallsCount += 1
getSpaceRoomRoomIdReceivedRoomId = roomId
DispatchQueue.main.async {
self.getSpaceRoomRoomIdReceivedInvocations.append(roomId)
}
if let getSpaceRoomRoomIdClosure = getSpaceRoomRoomIdClosure {
return try await getSpaceRoomRoomIdClosure(roomId)
} else {
return getSpaceRoomRoomIdReturnValue
}
}
//MARK: - joinedParentsOfChild //MARK: - joinedParentsOfChild
open var joinedParentsOfChildChildIdThrowableError: Error? open var joinedParentsOfChildChildIdThrowableError: Error?
@@ -34,6 +34,9 @@ extension SpaceServiceProxyMock {
.success(LeaveSpaceHandleProxy(spaceID: spaceID, .success(LeaveSpaceHandleProxy(spaceID: spaceID,
leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms)))) leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms))))
} }
spaceForIdentifierSpaceIDClosure = { spaceID in
.success(configuration.joinedSpaces.first { $0.id == spaceID })
}
} }
} }
@@ -37,7 +37,7 @@ enum ManageAuthorizedSpacesScreenViewAction {
} }
struct AuthorizedSpacesSelection { struct AuthorizedSpacesSelection {
let joinedParentSpaces: [SpaceRoomProxyProtocol] let joinedSpaces: [SpaceRoomProxyProtocol]
let unknownSpacesIDs: [String] let unknownSpacesIDs: [String]
let initialSelectedIDs: Set<String> let initialSelectedIDs: Set<String>
let selectedIDs: PassthroughSubject<Set<String>, Never> = .init() let selectedIDs: PassthroughSubject<Set<String>, Never> = .init()
@@ -15,8 +15,8 @@ struct ManageAuthorizedSpacesScreen: View {
Form { Form {
header header
if !context.viewState.authorizedSpacesSelection.joinedParentSpaces.isEmpty { if !context.viewState.authorizedSpacesSelection.joinedSpaces.isEmpty {
joinedParentsSection joinedSpacesSection
} }
if !context.viewState.authorizedSpacesSelection.unknownSpacesIDs.isEmpty { if !context.viewState.authorizedSpacesSelection.unknownSpacesIDs.isEmpty {
@@ -46,9 +46,9 @@ struct ManageAuthorizedSpacesScreen: View {
} }
} }
private var joinedParentsSection: some View { private var joinedSpacesSection: some View {
Section { Section {
ForEach(context.viewState.authorizedSpacesSelection.joinedParentSpaces, id: \.id) { space in ForEach(context.viewState.authorizedSpacesSelection.joinedSpaces, id: \.id) { space in
ListRow(label: .avatar(title: space.name, ListRow(label: .avatar(title: space.name,
description: space.canonicalAlias, description: space.canonicalAlias,
icon: avatar(space: space)), icon: avatar(space: space)),
@@ -102,7 +102,7 @@ struct ManageAuthorizedSpacesScreen: View {
// MARK: - Previews // MARK: - Previews
struct ManageAuthorizedSpacesScreen_Previews: PreviewProvider, TestablePreview { struct ManageAuthorizedSpacesScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = ManageAuthorizedSpacesScreenViewModel(authorizedSpacesSelection: .init(joinedParentSpaces: .mockJoinedSpaces2, static let viewModel = ManageAuthorizedSpacesScreenViewModel(authorizedSpacesSelection: .init(joinedSpaces: .mockJoinedSpaces2,
unknownSpacesIDs: ["!unknown-space-id-1", unknownSpacesIDs: ["!unknown-space-id-1",
"!unknown-space-id-2", "!unknown-space-id-2",
"!unknown-space-id-3"], "!unknown-space-id-3"],
@@ -38,11 +38,12 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
var canEditJoinRule = false var canEditJoinRule = false
var canEnableEncryption = false var canEnableEncryption = false
var canEditHistoryVisibility = false var canEditHistoryVisibility = false
var joinedParentSpaces: [SpaceRoomProxyProtocol] = [] /// The union of joined parent spaces and the joined spaces in the current access type
var selectableJoinedSpaces: [SpaceRoomProxyProtocol] = []
/// The count of the intersection between the set of joined parent spaces and the set of spaces in the current access type /// The count of the intersection between the set of joined parent spaces and the set of spaces in the current access type
var selectableSpacesCount: Int { var selectableSpacesCount: Int {
Set(joinedParentSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count Set(selectableJoinedSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count
} }
private var hasChanges: Bool { private var hasChanges: Bool {
@@ -85,8 +86,8 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
var spaceMembersDescription: String { var spaceMembersDescription: String {
if isSpaceMembersOptionSelectable { if isSpaceMembersOptionSelectable {
switch spaceSelection { switch spaceSelection {
case .singleJoined(let joinedParentSpace): case .singleJoined(let joinedSpace):
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedParentSpace.name) L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedSpace.name)
case .singleUnknown(let id): case .singleUnknown(let id):
L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(id) L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(id)
case .multiple, .empty: case .multiple, .empty:
@@ -100,8 +101,8 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
var askToJoinWithSpaceMembersDescription: String { var askToJoinWithSpaceMembersDescription: String {
if isAskToJoinWithSpaceMembersOptionSelectable { if isAskToJoinWithSpaceMembersOptionSelectable {
switch spaceSelection { switch spaceSelection {
case .singleJoined(let joinedParentSpace): case .singleJoined(let joinedSpace):
L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(joinedParentSpace.name) L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(joinedSpace.name)
case .singleUnknown(let id): case .singleUnknown(let id):
L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(id) L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(id)
case .multiple, .empty: case .multiple, .empty:
@@ -140,17 +141,17 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
.empty .empty
} else if selectableSpacesCount > 1 { } else if selectableSpacesCount > 1 {
.multiple .multiple
} else if let joinedParent = joinedParentSpaces.first { } else if let joinedSpace = selectableJoinedSpaces.first {
if currentSettings.accessType.isSpaceMembers || currentSettings.accessType.isAskToJoinWithSpaceMembers { if currentSettings.accessType.isSpaceMembers || currentSettings.accessType.isAskToJoinWithSpaceMembers {
if currentSettings.accessType.spaceIDs.isEmpty { if currentSettings.accessType.spaceIDs.isEmpty {
// Edge case where the access type is already space members, but it does not contain any id // Edge case where the access type is already space members, but it does not contain any id
// So if the user wants to add their own parent they need to do it from the selection menu // So if the user wants to add their own parent they need to do it from the selection menu
.multiple .multiple
} else { } else {
.singleJoined(joinedParent) .singleJoined(joinedSpace)
} }
} else { } else {
.singleJoined(joinedParent) .singleJoined(joinedSpace)
} }
} else if let unknownSpaceID = currentSettings.accessType.spaceIDs.first { } else if let unknownSpaceID = currentSettings.accessType.spaceIDs.first {
// The space is not joined by the user but is currently selected // The space is not joined by the user but is currently selected
@@ -47,12 +47,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
setupRoomDirectoryVisibility() setupRoomDirectoryVisibility()
setupSubscriptions() setupSubscriptions()
Task { Task {
switch await clientProxy.spaceService.joinedParents(childID: roomProxy.id) { await setupSelectableJoinedSpaces()
case .success(let joinedParentSpaces):
state.joinedParentSpaces = joinedParentSpaces
case .failure:
break
}
} }
} }
@@ -233,6 +228,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
if shouldDismiss, !hasFailures { if shouldDismiss, !hasFailures {
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} else if !shouldDismiss {
await setupSelectableJoinedSpaces()
} }
} }
@@ -243,8 +240,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
} }
switch state.spaceSelection { switch state.spaceSelection {
case .singleJoined(let joinedParent): case .singleJoined(let joinedSpace):
state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [joinedParent.id]) state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [joinedSpace.id])
case .singleUnknown(let id): case .singleUnknown(let id):
state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [id]) state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [id])
case .empty: case .empty:
@@ -261,8 +258,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
} }
switch state.spaceSelection { switch state.spaceSelection {
case .singleJoined(let joinedParent): case .singleJoined(let joinedSpace):
state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [joinedParent.id]) state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [joinedSpace.id])
case .singleUnknown(let id): case .singleUnknown(let id):
state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [id]) state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [id])
case .empty: case .empty:
@@ -273,12 +270,12 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
} }
private func displayManageAuthorizedSpacesScreen(isAskToJoin: Bool) { private func displayManageAuthorizedSpacesScreen(isAskToJoin: Bool) {
let joinedParentSpaces = state.joinedParentSpaces let joinedSpaces = state.selectableJoinedSpaces
let unknownSpaceIDs = state.currentSettings.accessType.spaceIDs.filter { id in let unknownSpaceIDs = state.currentSettings.accessType.spaceIDs.filter { id in
!joinedParentSpaces.contains { $0.id == id } !joinedSpaces.contains { $0.id == id }
} }
let selectedIDs = Set(state.bindings.desiredSettings.accessType.spaceIDs) let selectedIDs = Set(state.bindings.desiredSettings.accessType.spaceIDs)
let authorizedSpacesSelection = AuthorizedSpacesSelection(joinedParentSpaces: joinedParentSpaces, let authorizedSpacesSelection = AuthorizedSpacesSelection(joinedSpaces: joinedSpaces,
unknownSpacesIDs: unknownSpaceIDs, unknownSpacesIDs: unknownSpaceIDs,
initialSelectedIDs: selectedIDs) initialSelectedIDs: selectedIDs)
authorizedSpacesSelection.selectedIDs authorizedSpacesSelection.selectedIDs
@@ -291,6 +288,28 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
actionsSubject.send(.displayManageAuthorizedSpacesScreen(authorizedSpacesSelection)) actionsSubject.send(.displayManageAuthorizedSpacesScreen(authorizedSpacesSelection))
} }
private func setupSelectableJoinedSpaces() async {
var joinedParentSpaces: [SpaceRoomProxyProtocol] = []
switch await clientProxy.spaceService.joinedParents(childID: roomProxy.id) {
case .success(let value):
joinedParentSpaces = value
case .failure:
break
}
var nonParentJoinedSpaces: [SpaceRoomProxyProtocol] = []
for spaceID in state.currentSettings.accessType.spaceIDs where !joinedParentSpaces.contains(where: { $0.id == spaceID }) {
if case let .success(.some(space)) = await clientProxy.spaceService.spaceForIdentifier(spaceID: spaceID) {
nonParentJoinedSpaces.append(space)
}
}
// By default we only want to allow selection among joined parents but
// if there is a non parent joined space already set in the access type
// we also include it in the known spaces selection list.
state.selectableJoinedSpaces = joinedParentSpaces + nonParentJoinedSpaces
}
private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading" private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading"
private func showLoadingIndicator() { private func showLoadingIndicator() {
@@ -40,6 +40,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
} }
} }
func spaceForIdentifier(spaceID: String) async -> Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError> {
do {
return try await .success(spaceService.getSpaceRoom(roomId: spaceID).map(SpaceRoomProxy.init))
} catch {
MXLog.error("Failed getting space room for \(spaceID): \(error)")
return .failure(.sdkError(error))
}
}
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> { func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> {
do { do {
return try await .success(.init(spaceID: spaceID, leaveHandle: spaceService.leaveSpace(spaceId: spaceID))) return try await .success(.init(spaceID: spaceID, leaveHandle: spaceService.leaveSpace(spaceId: spaceID)))
@@ -18,6 +18,8 @@ protocol SpaceServiceProxyProtocol {
var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get } var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get }
func spaceRoomList(spaceID: String) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError> func spaceRoomList(spaceID: String) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>
/// Returns a joined space given its identifier
func spaceForIdentifier(spaceID: String) async -> Result<SpaceRoomProxyProtocol?, SpaceServiceProxyError>
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
/// Returns all the parent spaces of a child that user has joined. /// Returns all the parent spaces of a child that user has joined.
func joinedParents(childID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError> func joinedParents(childID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError>
@@ -15,6 +15,7 @@ import XCTest
@MainActor @MainActor
class SecurityAndPrivacyScreenViewModelTests: XCTestCase { class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
var viewModel: SecurityAndPrivacyScreenViewModelProtocol! var viewModel: SecurityAndPrivacyScreenViewModelProtocol!
var spaceServiceProxy: SpaceServiceProxyMock!
var roomProxy: JoinedRoomProxyMock! var roomProxy: JoinedRoomProxyMock!
var context: SecurityAndPrivacyScreenViewModelType.Context { var context: SecurityAndPrivacyScreenViewModelType.Context {
@@ -32,7 +33,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let space = singleRoom[0] let space = singleRoom[0]
setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public) setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public)
let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 1 } let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 1 }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
@@ -63,7 +64,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let space = singleRoom[0] let space = singleRoom[0]
setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public) setupViewModel(joinedParentSpaces: singleRoom, joinRule: .public)
let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 1 } let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 1 }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
@@ -94,7 +95,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let space = singleRoom[0] let space = singleRoom[0]
setupViewModel(joinedParentSpaces: [], joinRule: .restricted(rules: [.roomMembership(roomId: space.id)])) setupViewModel(joinedParentSpaces: [], joinRule: .restricted(rules: [.roomMembership(roomId: space.id)]))
let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 0 } let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 0 }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.currentSettings.accessType, .spaceMembers(spaceIDs: [space.id])) XCTAssertEqual(context.viewState.currentSettings.accessType, .spaceMembers(spaceIDs: [space.id]))
@@ -124,7 +125,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2
setupViewModel(joinedParentSpaces: spaces, joinRule: .public) setupViewModel(joinedParentSpaces: spaces, joinRule: .public)
let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 3 } let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 3 }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
@@ -140,7 +141,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
switch action { switch action {
case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection):
defer { selectedIDs = authorizedSpacesSelection.selectedIDs } defer { selectedIDs = authorizedSpacesSelection.selectedIDs }
return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) &&
authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.unknownSpacesIDs.isEmpty &&
authorizedSpacesSelection.initialSelectedIDs.isEmpty authorizedSpacesSelection.initialSelectedIDs.isEmpty
default: default:
@@ -168,7 +169,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2
setupViewModel(joinedParentSpaces: spaces, joinRule: .public) setupViewModel(joinedParentSpaces: spaces, joinRule: .public)
let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 3 } let deferred = deferFulfillment(context.$viewState) { $0.selectableJoinedSpaces.count == 3 }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone)
@@ -184,7 +185,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
switch action { switch action {
case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection):
defer { selectedIDs = authorizedSpacesSelection.selectedIDs } defer { selectedIDs = authorizedSpacesSelection.selectedIDs }
return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) &&
authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.unknownSpacesIDs.isEmpty &&
authorizedSpacesSelection.initialSelectedIDs.isEmpty authorizedSpacesSelection.initialSelectedIDs.isEmpty
default: default:
@@ -230,7 +231,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection):
// We need the // We need the
defer { selectedIDs = authorizedSpacesSelection.selectedIDs } defer { selectedIDs = authorizedSpacesSelection.selectedIDs }
return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) &&
authorizedSpacesSelection.unknownSpacesIDs == ["unknownSpaceID"] && authorizedSpacesSelection.unknownSpacesIDs == ["unknownSpaceID"] &&
authorizedSpacesSelection.initialSelectedIDs == ["unknownSpaceID"] authorizedSpacesSelection.initialSelectedIDs == ["unknownSpaceID"]
default: default:
@@ -254,6 +255,48 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
await fulfillment(of: [expectation]) await fulfillment(of: [expectation])
} }
func testMultipleSpacesMembersSelectionWithAnExistingNonParentButJoinedSpace() async throws {
let joinedParentSpaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2
let singleRoom = [SpaceRoomProxyProtocol].mockSingleRoom
let space = singleRoom[0]
let allSpaces = joinedParentSpaces + singleRoom
setupViewModel(joinedParentSpaces: joinedParentSpaces,
joinedSpaces: allSpaces,
joinRule: .restricted(rules: [.roomMembership(roomId: space.id),
.roomMembership(roomId: "unknownSpaceID")]))
let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 5 }
try await deferred.fulfill()
XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceMembers)
XCTAssertTrue(context.viewState.isSaveDisabled)
XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable)
guard case .multiple = context.viewState.spaceSelection else {
XCTFail("Expected spaceSelection to be .multiple")
return
}
var selectedIDs: PassthroughSubject<Set<String>, Never>!
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in
switch action {
case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection):
// We need the
defer { selectedIDs = authorizedSpacesSelection.selectedIDs }
return authorizedSpacesSelection.joinedSpaces.map(\.id) == allSpaces.map(\.id) &&
authorizedSpacesSelection.unknownSpacesIDs == ["unknownSpaceID"] &&
authorizedSpacesSelection.initialSelectedIDs == [space.id, "unknownSpaceID"]
default:
return false
}
}
context.send(viewAction: .manageSpaces)
try await deferredAction.fulfill()
selectedIDs.send([allSpaces[0].id, "unknownSpaceID"])
XCTAssertEqual(context.desiredSettings.accessType, .spaceMembers(spaceIDs: [allSpaces[0].id, "unknownSpaceID"]))
XCTAssertNotNil(context.viewState.accessSectionFooter)
XCTAssertFalse(context.viewState.isSaveDisabled)
}
func testEmptySpaceMembersSelectionEdgeCase() async throws { func testEmptySpaceMembersSelectionEdgeCase() async throws {
// Edge case where there is no available joined parents and the room has a restricted join rule. // Edge case where there is no available joined parents and the room has a restricted join rule.
// With no space ids in it // With no space ids in it
@@ -295,7 +338,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in
switch action { switch action {
case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection):
return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == singleRoom.map(\.id) && return authorizedSpacesSelection.joinedSpaces.map(\.id) == singleRoom.map(\.id) &&
authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.unknownSpacesIDs.isEmpty &&
authorizedSpacesSelection.initialSelectedIDs.isEmpty authorizedSpacesSelection.initialSelectedIDs.isEmpty
default: default:
@@ -382,6 +425,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
// MARK: - Helpers // MARK: - Helpers
private func setupViewModel(joinedParentSpaces: [SpaceRoomProxyProtocol], private func setupViewModel(joinedParentSpaces: [SpaceRoomProxyProtocol],
joinedSpaces: [SpaceRoomProxyProtocol] = [],
joinRule: JoinRule) { joinRule: JoinRule) {
let appSettings = AppSettings() let appSettings = AppSettings()
appSettings.spaceSettingsEnabled = true appSettings.spaceSettingsEnabled = true
@@ -396,7 +440,8 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase {
viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: roomProxy, viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: roomProxy,
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org", clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
spaceServiceConfiguration: .init(joinedParentSpaces: joinedParentSpaces))), spaceServiceConfiguration: .init(joinedSpaces: joinedSpaces,
joinedParentSpaces: joinedParentSpaces))),
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
appSettings: appSettings) appSettings: appSettings)
} }
+1 -1
View File
@@ -71,7 +71,7 @@ packages:
# Element/Matrix dependencies # Element/Matrix dependencies
MatrixRustSDK: MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 25.12.10 exactVersion: 25.12.11
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
Compound: Compound:
path: compound-ios path: compound-ios