diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift index ce0b38d5e..dc5c86c01 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenModels.swift @@ -5,6 +5,7 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import Foundation enum ManageAuthorizedSpacesScreenViewModelAction { @@ -16,7 +17,7 @@ struct ManageAuthorizedSpacesScreenViewState: BindableState { var desiredSelectedIDs: Set var hasChanges: Bool { - authorizedSpacesSelection.selectedIDs != desiredSelectedIDs + authorizedSpacesSelection.currentSelectedIDs != desiredSelectedIDs } var isDoneButtonDisabled: Bool { @@ -25,7 +26,7 @@ struct ManageAuthorizedSpacesScreenViewState: BindableState { init(authorizedSpacesSelection: AuthorizedSpacesSelection) { self.authorizedSpacesSelection = authorizedSpacesSelection - desiredSelectedIDs = authorizedSpacesSelection.selectedIDs + desiredSelectedIDs = authorizedSpacesSelection.currentSelectedIDs } } @@ -38,5 +39,6 @@ enum ManageAuthorizedSpacesScreenViewAction { struct AuthorizedSpacesSelection { let joinedParentSpaces: [SpaceRoomProxyProtocol] let unknownSpacesIDs: [String] - let selectedIDs: Set + let currentSelectedIDs: Set + let desiredSelectIDs: PassthroughSubject, Never> = .init() } diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift index e5f766ca4..f24cec5a9 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift @@ -30,8 +30,8 @@ class ManageAuthorizedSpacesScreenViewModel: ManageAuthorizedSpacesScreenViewMod case .cancel: actionsSubject.send(.dismiss) case .done: - // TODO: Implement - break + state.authorizedSpacesSelection.desiredSelectIDs.send(state.desiredSelectedIDs) + actionsSubject.send(.dismiss) case .toggle(let spaceID): if state.desiredSelectedIDs.contains(spaceID) { state.desiredSelectedIDs.remove(spaceID) diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift index b29d24cee..f274f53a7 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/View/ManageAuthorizedSpacesScreen.swift @@ -104,9 +104,9 @@ struct ManageAuthorizedSpacesScreen_Previews: PreviewProvider, TestablePreview { unknownSpacesIDs: ["!unknown-space-id-1", "!unknown-space-id-2", "!unknown-space-id-3"], - selectedIDs: ["space1", - "space3", - "!unknown-space-id-2"]), + currentSelectedIDs: ["space1", + "space3", + "!unknown-space-id-2"]), mediaProvider: MediaProviderMock(configuration: .init())) static var previews: some View { diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift index c77b0bbaf..17acbb1cf 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift @@ -252,12 +252,19 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, private func displayManageAuthorizedSpacesScreen() { let joinedParentSpaces = state.joinedParentSpaces let unknownSpaceIDs = state.currentSettings.accessType.spaceIDs.filter { id in - !joinedParentSpaces.contains(where: { $0.id == id }) + !joinedParentSpaces.contains { $0.id == id } } let selectedIDs = Set(state.bindings.desiredSettings.accessType.spaceIDs) - actionsSubject.send(.displayManageAuthorizedSpacesScreen(.init(joinedParentSpaces: joinedParentSpaces, - unknownSpacesIDs: unknownSpaceIDs, - selectedIDs: selectedIDs))) + let authorizedSpacesSelection = AuthorizedSpacesSelection(joinedParentSpaces: joinedParentSpaces, + unknownSpacesIDs: unknownSpaceIDs, + currentSelectedIDs: selectedIDs) + authorizedSpacesSelection.desiredSelectIDs + .sink { [weak self] desiredSelectedIDs in + self?.state.bindings.desiredSettings.accessType = .spaceUsers(spaceIDs: desiredSelectedIDs.sorted()) + } + .store(in: &cancellables) + + actionsSubject.send(.displayManageAuthorizedSpacesScreen(authorizedSpacesSelection)) } private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading" diff --git a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift index 8a69aa6d4..27ffc22ad 100644 --- a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift +++ b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift @@ -6,6 +6,7 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import MatrixRustSDK import XCTest @@ -88,6 +89,96 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { } } + func testMultipleKnownSpacesMembersSelection() async throws { + let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 + setupViewModel(isSpaceSettingsEnabled: true, joinedParentSpaces: spaces, joinRule: .public) + + let deferred = deferFulfillment(context.$viewState) { $0.joinedParentSpaces.count == 3 } + try await deferred.fulfill() + + XCTAssertEqual(context.viewState.currentSettings.accessType, .anyone) + XCTAssertTrue(context.viewState.isSaveDisabled) + XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable) + guard case .multiple = context.viewState.spaceSelection else { + XCTFail("Expected spaceSelection to be .singleSpace") + return + } + + var desiredSpaceIDs: PassthroughSubject, Never>! + let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in + switch action { + case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): + defer { desiredSpaceIDs = authorizedSpacesSelection.desiredSelectIDs } + return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && + authorizedSpacesSelection.unknownSpacesIDs.isEmpty && + authorizedSpacesSelection.currentSelectedIDs.isEmpty + default: + return false + } + } + context.send(viewAction: .selectedSpaceMembersAccess) + try await deferredAction.fulfill() + desiredSpaceIDs.send([spaces[0].id]) + XCTAssertEqual(context.desiredSettings.accessType, .spaceUsers(spaceIDs: [spaces[0].id])) + XCTAssertNotNil(context.viewState.accessSectionFooter) + XCTAssertFalse(context.viewState.isSaveDisabled) + + let expectation = expectation(description: "Join rule has updated") + roomProxy.updateJoinRuleClosure = { value in + XCTAssertEqual(value, .restricted(rules: [.roomMembership(roomId: spaces[0].id)])) + expectation.fulfill() + return .success(()) + } + context.send(viewAction: .save) + await fulfillment(of: [expectation]) + } + + func testMultipleSpacesMembersSelection() async throws { + let spaces = [SpaceRoomProxyProtocol].mockJoinedSpaces2 + setupViewModel(isSpaceSettingsEnabled: true, + joinedParentSpaces: spaces, + joinRule: .restricted(rules: [.roomMembership(roomId: "unknownSpaceID")])) + + let deferred = deferFulfillment(context.$viewState) { $0.selectableSpacesCount == 4 } + try await deferred.fulfill() + + XCTAssertTrue(context.viewState.currentSettings.accessType.isSpaceUsers) + XCTAssertTrue(context.viewState.isSaveDisabled) + XCTAssertTrue(context.viewState.isSpaceMembersOptionSelectable) + guard case .multiple = context.viewState.spaceSelection else { + XCTFail("Expected spaceSelection to be .singleSpace") + return + } + + var desiredSpaceIDs: PassthroughSubject, Never>! + let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in + switch action { + case .displayManageAuthorizedSpacesScreen(let authorizedSpacesSelection): + defer { desiredSpaceIDs = authorizedSpacesSelection.desiredSelectIDs } + return authorizedSpacesSelection.joinedParentSpaces.map(\.id) == spaces.map(\.id) && + authorizedSpacesSelection.unknownSpacesIDs == ["unknownSpaceID"] && + authorizedSpacesSelection.currentSelectedIDs == ["unknownSpaceID"] + default: + return false + } + } + context.send(viewAction: .manageSpaces) + try await deferredAction.fulfill() + desiredSpaceIDs.send([spaces[0].id, "unknownSpaceID"]) + XCTAssertEqual(context.desiredSettings.accessType, .spaceUsers(spaceIDs: [spaces[0].id, "unknownSpaceID"])) + XCTAssertNotNil(context.viewState.accessSectionFooter) + XCTAssertFalse(context.viewState.isSaveDisabled) + + let expectation = expectation(description: "Join rule has updated") + roomProxy.updateJoinRuleClosure = { value in + XCTAssertEqual(value, .restricted(rules: [.roomMembership(roomId: spaces[0].id), .roomMembership(roomId: "unknownSpaceID")])) + expectation.fulfill() + return .success(()) + } + context.send(viewAction: .save) + await fulfillment(of: [expectation]) + } + func testSave() async throws { setupViewModel(isSpaceSettingsEnabled: false, joinedParentSpaces: [], joinRule: .public) @@ -110,7 +201,14 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { XCTAssertNotNil(context.alertInfo) - let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .dismiss } + let deferred = deferFulfillment(viewModel.actionsPublisher) { + switch $0 { + case .dismiss: + true + default: + false + } + } context.alertInfo?.secondaryButton?.action?() // Discard try await deferred.fulfill() } @@ -125,7 +223,14 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { XCTAssertNotNil(context.alertInfo) - let deferred = deferFulfillment(viewModel.actionsPublisher) { $0 == .dismiss } + let deferred = deferFulfillment(viewModel.actionsPublisher) { + switch $0 { + case .dismiss: + true + default: + false + } + } context.alertInfo?.primaryButton.action?() // Save try await deferred.fulfill() } @@ -160,6 +265,7 @@ class SecurityAndPrivacyScreenViewModelTests: XCTestCase { joinRule: joinRule, isVisibleInPublicDirectory: true)) roomProxy.updateJoinRuleReturnValue = .success(()) + roomProxy.updateRoomDirectoryVisibilityReturnValue = .success(()) viewModel = SecurityAndPrivacyScreenViewModel(roomProxy: roomProxy, clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",