implemented the navigation to the manage auth spaces screen

# Conflicts:
#	ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
#	ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift
#	ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift
#	ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift
This commit is contained in:
Mauro Romito
2025-11-27 17:36:34 +01:00
committed by Mauro
parent 2556eccc29
commit 0dcf1ffda2
15 changed files with 340 additions and 51 deletions
@@ -472,6 +472,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomDetails, .presentSecurityAndPrivacyScreen, .securityAndPrivacy):
presentSecurityAndPrivacyScreen()
case (.securityAndPrivacy, .presentManageAuthorizedSpacesScreen, .manageAuthorizedSpacesScreen):
guard let selection = (context.userInfo as? EventUserInfo)?.authorizedSpacesSelection else {
fatalError("Missing required AuthorizedSpacesSelection")
}
presentManageAuthorizedSpacesScreen(selection: selection)
case (.roomDetails, .presentReportRoomScreen, .reportRoom):
presentReportRoom()
@@ -1357,6 +1363,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentEditAddressScreen()
case .dismiss:
navigationStackCoordinator.pop()
case .displayManageAuthorizedSpacesScreen(let selection):
stateMachine.tryEvent(.presentManageAuthorizedSpacesScreen, userInfo: EventUserInfo(animated: true, authorizedSpacesSelection: selection))
}
}
.store(in: &cancellables)
@@ -1366,6 +1374,25 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentManageAuthorizedSpacesScreen(selection: AuthorizedSpacesSelection) {
let navigationStack = NavigationStackCoordinator()
let coordinator = ManageAuthorizedSpacesScreenCoordinator(parameters: .init(authorizedSpacesSelection: selection,
mediaProvider: flowParameters.userSession.mediaProvider))
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStack.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(navigationStack) { [weak self] in
self?.stateMachine.tryEvent(.dismissedManageAuthorizedSpacesScreen)
}
}
private func presentEditAddressScreen() {
let stackCoordinator = NavigationStackCoordinator()
let coordinator = EditRoomAddressScreenCoordinator(parameters: .init(roomProxy: roomProxy,
@@ -80,6 +80,7 @@ extension RoomFlowCoordinator {
case knockRequestsList(previousState: State)
case mediaEventsTimeline(previousState: State)
case securityAndPrivacy(previousState: State)
case manageAuthorizedSpacesScreen(previousState: State)
case reportRoom(previousState: State)
case declineAndBlockScreen
@@ -98,6 +99,7 @@ extension RoomFlowCoordinator {
let animated: Bool
var timelineController: TimelineControllerProtocol?
var spaceRoomListProxy: SpaceRoomListProxyProtocol?
var authorizedSpacesSelection: AuthorizedSpacesSelection?
}
enum Event: EventType {
@@ -174,6 +176,9 @@ extension RoomFlowCoordinator {
case presentSecurityAndPrivacyScreen
case dismissSecurityAndPrivacyScreen
case presentManageAuthorizedSpacesScreen
case dismissedManageAuthorizedSpacesScreen
case presentReportRoomScreen
case dismissReportRoomScreen
@@ -321,6 +326,11 @@ extension RoomFlowCoordinator {
case (.securityAndPrivacy(let previousState), .dismissSecurityAndPrivacyScreen):
return previousState
case (.securityAndPrivacy, .presentManageAuthorizedSpacesScreen):
return .manageAuthorizedSpacesScreen(previousState: fromState)
case (.manageAuthorizedSpacesScreen(let previousState), .dismissedManageAuthorizedSpacesScreen):
return previousState
case (.roomDetails, .presentReportRoomScreen):
return .reportRoom(previousState: fromState)
case (.reportRoom(let previousState), .dismissReportRoomScreen):
@@ -1,4 +1,3 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
@@ -33,6 +32,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
case rolesAndPermissionsFlow
/// The members flow screen
case membersFlow
case manageAuthorizedSpacesScreen
}
enum Event: EventType {
@@ -55,6 +56,9 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
case startRolesAndPermissionsFlow
case stopRolesAndPermissionsFlow
case presentManageAuthorizedSpacesScreen
case dismissedManageAuthorizedSpacesScreen
}
private let roomProxy: JoinedRoomProxyProtocol
@@ -126,6 +130,11 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
case (.editAddress, .dismissedEditAddress):
return .securityAndPrivacy
case (.securityAndPrivacy, .presentManageAuthorizedSpacesScreen):
return .manageAuthorizedSpacesScreen
case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen):
return .securityAndPrivacy
case (.spaceSettings, .startMembersListFlow):
return .membersFlow
case (.membersFlow, .stopMembersListFlow):
@@ -164,6 +173,14 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
case (.editAddress, .dismissedEditAddress, .securityAndPrivacy):
break
case (.securityAndPrivacy, .presentManageAuthorizedSpacesScreen, .manageAuthorizedSpacesScreen):
guard let selection = context.userInfo as? AuthorizedSpacesSelection else {
fatalError("AuthorizedSpacesSelection expected as userInfo")
}
presentManageAuthorizedSpacesScreen(selection: selection)
case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen, .securityAndPrivacy):
break
case (.spaceSettings, .startMembersListFlow, .membersFlow):
startMembersListFlow()
case (.membersFlow, .stopMembersListFlow, .spaceSettings):
@@ -258,6 +275,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
self.stateMachine.tryEvent(.presentEditAddress)
case .dismiss:
navigationStackCoordinator.pop()
case .displayManageAuthorizedSpacesScreen(let selection):
self.stateMachine.tryEvent(.presentManageAuthorizedSpacesScreen, userInfo: selection)
}
}
.store(in: &cancellables)
@@ -287,6 +306,25 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentManageAuthorizedSpacesScreen(selection: AuthorizedSpacesSelection) {
let navigationStack = NavigationStackCoordinator()
let coordinator = ManageAuthorizedSpacesScreenCoordinator(parameters: .init(authorizedSpacesSelection: selection,
mediaProvider: flowParameters.userSession.mediaProvider))
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStack.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(navigationStack) { [weak self] in
self?.stateMachine.tryEvent(.dismissedManageAuthorizedSpacesScreen)
}
}
// MARK: - Other flows
private func startRolesAndPermissionsFlow() {
@@ -82,6 +82,7 @@ enum TestablePreviewsDictionary {
"LocationRoomTimelineView_Previews" : LocationRoomTimelineView_Previews.self,
"LoginScreen_Previews" : LoginScreen_Previews.self,
"LongPressWithFeedback_Previews" : LongPressWithFeedback_Previews.self,
"ManageAuthorizedSpacesScreen_Previews" : ManageAuthorizedSpacesScreen_Previews.self,
"ManageRoomMemberSheetView_Previews" : ManageRoomMemberSheetView_Previews.self,
"MapLibreStaticMapView_Previews" : MapLibreStaticMapView_Previews.self,
"MatrixUserPermalink_Previews" : MatrixUserPermalink_Previews.self,
@@ -0,0 +1,53 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
// periphery:ignore:all - this is just a manageAuthorizedSpaces remove this comment once generating the final file
import Combine
import SwiftUI
struct ManageAuthorizedSpacesScreenCoordinatorParameters {
let authorizedSpacesSelection: AuthorizedSpacesSelection
let mediaProvider: MediaProviderProtocol
}
enum ManageAuthorizedSpacesScreenCoordinatorAction {
case dismiss
}
final class ManageAuthorizedSpacesScreenCoordinator: CoordinatorProtocol {
private let viewModel: ManageAuthorizedSpacesScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<ManageAuthorizedSpacesScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<ManageAuthorizedSpacesScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: ManageAuthorizedSpacesScreenCoordinatorParameters) {
viewModel = ManageAuthorizedSpacesScreenViewModel(authorizedSpacesSelection: parameters.authorizedSpacesSelection,
mediaProvider: parameters.mediaProvider)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .dismiss:
actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(ManageAuthorizedSpacesScreen(context: viewModel.context))
}
}
@@ -0,0 +1,34 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
enum ManageAuthorizedSpacesScreenViewModelAction {
case dismiss
}
struct ManageAuthorizedSpacesScreenViewState: BindableState {
let authorizedSpacesSelection: AuthorizedSpacesSelection
var desiredSelectedIDs: Set<String>
var hasChanges: Bool {
authorizedSpacesSelection.selectedIDs != desiredSelectedIDs
}
init(authorizedSpacesSelection: AuthorizedSpacesSelection) {
self.authorizedSpacesSelection = authorizedSpacesSelection
desiredSelectedIDs = authorizedSpacesSelection.selectedIDs
}
}
enum ManageAuthorizedSpacesScreenViewAction { }
struct AuthorizedSpacesSelection {
let joinedParentSpaces: [SpaceRoomProxyProtocol]
let unknownSpacesIDs: [String]
let selectedIDs: Set<String>
}
@@ -0,0 +1,30 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias ManageAuthorizedSpacesScreenViewModelType = StateStoreViewModelV2<ManageAuthorizedSpacesScreenViewState, ManageAuthorizedSpacesScreenViewAction>
class ManageAuthorizedSpacesScreenViewModel: ManageAuthorizedSpacesScreenViewModelType, ManageAuthorizedSpacesScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<ManageAuthorizedSpacesScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<ManageAuthorizedSpacesScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(authorizedSpacesSelection: AuthorizedSpacesSelection,
mediaProvider: MediaProviderProtocol) {
super.init(initialViewState: ManageAuthorizedSpacesScreenViewState(authorizedSpacesSelection: authorizedSpacesSelection),
mediaProvider: mediaProvider)
}
// MARK: - Public
override func process(viewAction: ManageAuthorizedSpacesScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
}
}
@@ -0,0 +1,14 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
@MainActor
protocol ManageAuthorizedSpacesScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ManageAuthorizedSpacesScreenViewModelAction, Never> { get }
var context: ManageAuthorizedSpacesScreenViewModelType.Context { get }
}
@@ -0,0 +1,36 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct ManageAuthorizedSpacesScreen: View {
@Bindable var context: ManageAuthorizedSpacesScreenViewModel.Context
var body: some View {
Form { }
.compoundList()
.navigationTitle("Manage spaces")
}
}
// MARK: - Previews
struct ManageAuthorizedSpacesScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = ManageAuthorizedSpacesScreenViewModel(authorizedSpacesSelection: .init(joinedParentSpaces: .mockJoinedSpaces,
unknownSpacesIDs: ["!unknown-space-id-1",
"!unknown-space-id-2",
"!unknown-space-id-3"],
selectedIDs: ["space1",
"space3",
"!unknown-space-id-2"]),
mediaProvider: MediaProviderMock(configuration: .init()))
static var previews: some View {
ManageAuthorizedSpacesScreen(context: viewModel.context)
}
}
@@ -19,6 +19,7 @@ struct SecurityAndPrivacyScreenCoordinatorParameters {
enum SecurityAndPrivacyScreenCoordinatorAction {
case displayEditAddressScreen
case dismiss
case displayManageAuthorizedSpacesScreen(AuthorizedSpacesSelection)
}
final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol {
@@ -44,6 +45,8 @@ final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .displayManageAuthorizedSpacesScreen(let selection):
actionsSubject.send(.displayManageAuthorizedSpacesScreen(selection))
case .displayEditAddressScreen:
actionsSubject.send(.displayEditAddressScreen)
case .dismiss:
@@ -11,6 +11,7 @@ import Foundation
enum SecurityAndPrivacyScreenViewModelAction {
case displayEditAddressScreen
case dismiss
case displayManageAuthorizedSpacesScreen(AuthorizedSpacesSelection)
}
struct SecurityAndPrivacyScreenViewState: BindableState {
@@ -81,8 +81,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
case .selectedSpaceMembersAccess:
handleSelectedSpaceMembersAccess()
case .manageSpaces:
// TODO: Implement multiple space selection
break
displayManageAuthorizedSpacesScreen()
}
}
@@ -236,15 +235,29 @@ 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
guard !state.bindings.desiredSettings.accessType.isSpaceUsers else {
return
}
switch state.spaceSelection {
case .singleJoined(let joinedParent):
state.bindings.desiredSettings.accessType = .spaceUsers(spaceIDs: [joinedParent.id])
case .singleUnknown(let id):
state.bindings.desiredSettings.accessType = .spaceUsers(spaceIDs: [id])
case .multiple:
displayManageAuthorizedSpacesScreen()
}
}
private func displayManageAuthorizedSpacesScreen() {
let joinedParentSpaces = state.joinedParentSpaces
let unknownSpaceIDs = state.currentSettings.accessType.spaceIDs.filter { id in
!joinedParentSpaces.contains(where: { $0.id == id })
}
let selectedIDs = Set(state.bindings.desiredSettings.accessType.spaceIDs)
actionsSubject.send(.displayManageAuthorizedSpacesScreen(.init(joinedParentSpaces: joinedParentSpaces,
unknownSpacesIDs: unknownSpaceIDs,
selectedIDs: selectedIDs)))
}
private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading"