diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 78ce0ba74..0e707876d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -9570,7 +9570,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 25.12.10; + version = 25.12.11; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c77277b2e..002fc30c3 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "ae9a08f7faebb5cd2af7fe29860378009066a27e", - "version" : "25.12.10" + "revision" : "8efbdc808f699f6df830f552a2b302e91d45431a", + "version" : "25.12.11" } }, { diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 8769ae547..1110d6c16 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -16568,6 +16568,76 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable { 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! + var spaceForIdentifierSpaceIDReturnValue: Result! { + get { + if Thread.isMainThread { + return spaceForIdentifierSpaceIDUnderlyingReturnValue + } else { + var returnValue: Result? = 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)? + + func spaceForIdentifier(spaceID: String) async -> Result { + spaceForIdentifierSpaceIDCallsCount += 1 + spaceForIdentifierSpaceIDReceivedSpaceID = spaceID + DispatchQueue.main.async { + self.spaceForIdentifierSpaceIDReceivedInvocations.append(spaceID) + } + if let spaceForIdentifierSpaceIDClosure = spaceForIdentifierSpaceIDClosure { + return await spaceForIdentifierSpaceIDClosure(spaceID) + } else { + return spaceForIdentifierSpaceIDReturnValue + } + } //MARK: - leaveSpace var leaveSpaceSpaceIDUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 9b9e564f6..761a170ee 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -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 open var joinedParentsOfChildChildIdThrowableError: Error? diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index a8ab9dbc5..d9d188bdb 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -34,6 +34,9 @@ extension SpaceServiceProxyMock { .success(LeaveSpaceHandleProxy(spaceID: spaceID, leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms)))) } + spaceForIdentifierSpaceIDClosure = { spaceID in + .success(configuration.joinedSpaces.first { $0.id == spaceID }) + } } } diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift index 4124b673a..6142ee892 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift @@ -37,7 +37,7 @@ enum ManageAuthorizedSpacesScreenViewAction { } struct AuthorizedSpacesSelection { - let joinedParentSpaces: [SpaceRoomProxyProtocol] + let joinedSpaces: [SpaceRoomProxyProtocol] let unknownSpacesIDs: [String] let initialSelectedIDs: Set let selectedIDs: PassthroughSubject, Never> = .init() diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift index e90f6b5e9..36ecd6761 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift @@ -15,8 +15,8 @@ struct ManageAuthorizedSpacesScreen: View { Form { header - if !context.viewState.authorizedSpacesSelection.joinedParentSpaces.isEmpty { - joinedParentsSection + if !context.viewState.authorizedSpacesSelection.joinedSpaces.isEmpty { + joinedSpacesSection } if !context.viewState.authorizedSpacesSelection.unknownSpacesIDs.isEmpty { @@ -46,9 +46,9 @@ struct ManageAuthorizedSpacesScreen: View { } } - private var joinedParentsSection: some View { + private var joinedSpacesSection: some View { 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, description: space.canonicalAlias, icon: avatar(space: space)), @@ -102,7 +102,7 @@ struct ManageAuthorizedSpacesScreen: View { // MARK: - Previews 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", "!unknown-space-id-2", "!unknown-space-id-3"], diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift index 0d76c40f4..c2f1154fc 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift @@ -38,11 +38,12 @@ struct SecurityAndPrivacyScreenViewState: BindableState { var canEditJoinRule = false var canEnableEncryption = 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 var selectableSpacesCount: Int { - Set(joinedParentSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count + Set(selectableJoinedSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count } private var hasChanges: Bool { @@ -85,8 +86,8 @@ struct SecurityAndPrivacyScreenViewState: BindableState { var spaceMembersDescription: String { if isSpaceMembersOptionSelectable { switch spaceSelection { - case .singleJoined(let joinedParentSpace): - L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedParentSpace.name) + case .singleJoined(let joinedSpace): + L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedSpace.name) case .singleUnknown(let id): L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(id) case .multiple, .empty: @@ -100,8 +101,8 @@ struct SecurityAndPrivacyScreenViewState: BindableState { var askToJoinWithSpaceMembersDescription: String { if isAskToJoinWithSpaceMembersOptionSelectable { switch spaceSelection { - case .singleJoined(let joinedParentSpace): - L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(joinedParentSpace.name) + case .singleJoined(let joinedSpace): + L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(joinedSpace.name) case .singleUnknown(let id): L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(id) case .multiple, .empty: @@ -140,17 +141,17 @@ struct SecurityAndPrivacyScreenViewState: BindableState { .empty } else if selectableSpacesCount > 1 { .multiple - } else if let joinedParent = joinedParentSpaces.first { + } else if let joinedSpace = selectableJoinedSpaces.first { if currentSettings.accessType.isSpaceMembers || currentSettings.accessType.isAskToJoinWithSpaceMembers { if currentSettings.accessType.spaceIDs.isEmpty { // 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 .multiple } else { - .singleJoined(joinedParent) + .singleJoined(joinedSpace) } } else { - .singleJoined(joinedParent) + .singleJoined(joinedSpace) } } else if let unknownSpaceID = currentSettings.accessType.spaceIDs.first { // The space is not joined by the user but is currently selected diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift index 17b7e8e40..15fa19f2b 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift @@ -47,12 +47,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, setupRoomDirectoryVisibility() setupSubscriptions() Task { - switch await clientProxy.spaceService.joinedParents(childID: roomProxy.id) { - case .success(let joinedParentSpaces): - state.joinedParentSpaces = joinedParentSpaces - case .failure: - break - } + await setupSelectableJoinedSpaces() } } @@ -233,6 +228,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, if shouldDismiss, !hasFailures { actionsSubject.send(.dismiss) + } else if !shouldDismiss { + await setupSelectableJoinedSpaces() } } @@ -243,8 +240,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, } switch state.spaceSelection { - case .singleJoined(let joinedParent): - state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [joinedParent.id]) + case .singleJoined(let joinedSpace): + state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [joinedSpace.id]) case .singleUnknown(let id): state.bindings.desiredSettings.accessType = .spaceMembers(spaceIDs: [id]) case .empty: @@ -261,8 +258,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, } switch state.spaceSelection { - case .singleJoined(let joinedParent): - state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [joinedParent.id]) + case .singleJoined(let joinedSpace): + state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [joinedSpace.id]) case .singleUnknown(let id): state.bindings.desiredSettings.accessType = .askToJoinWithSpaceMembers(spaceIDs: [id]) case .empty: @@ -273,12 +270,12 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, } private func displayManageAuthorizedSpacesScreen(isAskToJoin: Bool) { - let joinedParentSpaces = state.joinedParentSpaces + let joinedSpaces = state.selectableJoinedSpaces 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 authorizedSpacesSelection = AuthorizedSpacesSelection(joinedParentSpaces: joinedParentSpaces, + let authorizedSpacesSelection = AuthorizedSpacesSelection(joinedSpaces: joinedSpaces, unknownSpacesIDs: unknownSpaceIDs, initialSelectedIDs: selectedIDs) authorizedSpacesSelection.selectedIDs @@ -291,6 +288,28 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, 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 func showLoadingIndicator() { diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift index c4a641005..2722fda9c 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxy.swift @@ -40,6 +40,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol { } } + func spaceForIdentifier(spaceID: String) async -> Result { + 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 { do { return try await .success(.init(spaceID: spaceID, leaveHandle: spaceService.leaveSpace(spaceId: spaceID))) diff --git a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift index 35a862a5d..624e548ed 100644 --- a/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Spaces/SpaceServiceProxyProtocol.swift @@ -18,6 +18,8 @@ protocol SpaceServiceProxyProtocol { var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get } func spaceRoomList(spaceID: String) async -> Result + /// Returns a joined space given its identifier + func spaceForIdentifier(spaceID: String) async -> Result func leaveSpace(spaceID: String) async -> Result /// Returns all the parent spaces of a child that user has joined. func joinedParents(childID: String) async -> Result<[SpaceRoomProxyProtocol], SpaceServiceProxyError> diff --git a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift index 19f45d5df..4efbfde38 100644 --- a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift +++ b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift @@ -15,6 +15,7 @@ import XCTest @MainActor class SecurityAndPrivacyScreenViewModelTests: XCTestCase { var viewModel: SecurityAndPrivacyScreenViewModelProtocol! + var spaceServiceProxy: SpaceServiceProxyMock! var roomProxy: JoinedRoomProxyMock! var context: SecurityAndPrivacyScreenViewModelType.Context { @@ -32,7 +33,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let space = singleRoom[0] 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() XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) @@ -63,7 +64,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let space = singleRoom[0] 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() XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) @@ -94,7 +95,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let space = singleRoom[0] 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() XCTAssertEqual(context.viewState.currentSettings.accessType, .spaceMembers(spaceIDs: [space.id])) @@ -124,7 +125,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 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() XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) @@ -140,7 +141,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { switch action { case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): defer { selectedIDs = authorizedSpacesSelection.selectedIDs } - return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && + return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) && authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.initialSelectedIDs.isEmpty default: @@ -168,7 +169,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 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() XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) @@ -184,7 +185,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { switch action { case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): defer { selectedIDs = authorizedSpacesSelection.selectedIDs } - return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && + return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) && authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.initialSelectedIDs.isEmpty default: @@ -230,7 +231,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): // We need the defer { selectedIDs = authorizedSpacesSelection.selectedIDs } - return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && + return authorizedSpacesSelection.joinedSpaces.map(\.id) == spaces.map(\.id) && authorizedSpacesSelection.unknownSpacesIDs == ["unknownSpaceID"] && authorizedSpacesSelection.initialSelectedIDs == ["unknownSpaceID"] default: @@ -254,6 +255,48 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { 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, 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 { // Edge case where there is no available joined parents and the room has a restricted join rule. // With no space ids in it @@ -295,7 +338,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in switch action { case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): - return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == singleRoom.map(\.id) && + return authorizedSpacesSelection.joinedSpaces.map(\.id) == singleRoom.map(\.id) && authorizedSpacesSelection.unknownSpacesIDs.isEmpty && authorizedSpacesSelection.initialSelectedIDs.isEmpty default: @@ -382,6 +425,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { // MARK: - Helpers private func setupViewModel(joinedParentSpaces: [SpaceRoomProxyProtocol], + joinedSpaces: [SpaceRoomProxyProtocol] = [], joinRule: JoinRule) { let appSettings = AppSettings() appSettings.spaceSettingsEnabled = true @@ -396,7 +440,8 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: roomProxy, clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org", - spaceServiceConfiguration: .init(joinedParentSpaces: joinedParentSpaces))), + spaceServiceConfiguration: .init(joinedSpaces: joinedSpaces, + joinedParentSpaces: joinedParentSpaces))), userIndicatorController: UserIndicatorControllerMock(), appSettings: appSettings) } diff --git a/project.yml b/project.yml index 52f70c039..a86189292 100644 --- a/project.yml +++ b/project.yml @@ -71,7 +71,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 25.12.10 + exactVersion: 25.12.11 # path: ../matrix-rust-sdk Compound: path: compound-ios