diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift index 1b8a05c22..c65ebca33 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift @@ -29,6 +29,7 @@ struct CreateRoomScreenViewState: BindableState { var roomName: String let serverName: String let isKnockingFeatureEnabled: Bool + let canSelectSpace: Bool var aliasLocalPart: String var bindings: CreateRoomScreenViewStateBindings var avatarMediaInfo: MediaInfo? { @@ -59,18 +60,46 @@ struct CreateRoomScreenViewState: BindableState { } } - var availableAccessTypes: [CreateRoomAccessType] { - var availableTypes = CreateRoomAccessType.allCases - if isSpace || !isKnockingFeatureEnabled { - availableTypes.removeAll { $0 == .askToJoin } + var selectedSpace: SpaceServiceRoomProtocol? + + var availableAccessTypes: [CreateRoomScreenAccessType] { + var availableAccessTypes: [CreateRoomScreenAccessType] = [] + if isSpace { + availableAccessTypes = [.public] + } else if let selectedSpace, selectedSpace.joinRule != .public { + availableAccessTypes = [.spaceMembers] + if isKnockingFeatureEnabled { + availableAccessTypes.append(.askToJoinWithSpaceMembers) + } + } else { + availableAccessTypes = [.public] + if isKnockingFeatureEnabled { + availableAccessTypes.append(.askToJoin) + } + } + availableAccessTypes.append(.private) + return availableAccessTypes + } + + var roomAccessType: CreateRoomAccessType { + switch bindings.selectedAccessType { + case .public: + return .public + case .spaceMembers: + return .spaceMembers(spaceID: selectedSpace?.id ?? "") + case .askToJoinWithSpaceMembers: + return .askToJoinWithSpaceMembers(spaceID: selectedSpace?.id ?? "") + case .askToJoin: + return .askToJoin + case .private: + return .private } - return availableTypes } } struct CreateRoomScreenViewStateBindings { var roomTopic: String - var selectedAccessType: CreateRoomAccessType + var selectedAccessType: CreateRoomScreenAccessType var showAttachmentConfirmationDialog = false /// Information describing the currently displayed alert. @@ -102,3 +131,11 @@ extension Set { return nil } } + +enum CreateRoomScreenAccessType { + case `public` + case spaceMembers + case askToJoinWithSpaceMembers + case askToJoin + case `private` +} diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift index 5c60c970f..c97b37516 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift @@ -45,6 +45,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen roomName: "", serverName: userSession.clientProxy.userIDServerName ?? "", isKnockingFeatureEnabled: appSettings.knockingEnabled, + canSelectSpace: isSpace ? false : appSettings.createSpaceEnabled, aliasLocalPart: roomAliasNameFromRoomDisplayName(roomName: ""), bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -114,9 +115,9 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen // Reset the state related to public rooms if the user choses the room to be empty context.$viewState .dropFirst() - .map(\.bindings.selectedAccessType) - .removeDuplicates() + .map(\.roomAccessType) .filter(\.isPrivate) + .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } @@ -135,7 +136,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen return } - guard !state.bindings.selectedAccessType.isPrivate, + guard !state.roomAccessType.isPrivate, let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: aliasLocalPart, serverName: state.serverName) else { // While is empty or private room we don't change or display the error @@ -167,6 +168,18 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen } } .store(in: &cancellables) + + context.$viewState + .map(\.availableAccessTypes) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] availableAccessTypes in + guard let self else { return } + if !availableAccessTypes.contains(state.bindings.selectedAccessType) { + state.bindings.selectedAccessType = .private + } + } + .store(in: &cancellables) } private func createRoom() async { @@ -176,7 +189,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen showLoadingIndicator() // Better to double check the errors also when trying to create the room - if !state.bindings.selectedAccessType.isPrivate { + if !state.roomAccessType.isPrivate { guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: state.aliasLocalPart, serverName: state.serverName), isRoomAliasFormatValid(alias: canonicalAlias) else { @@ -224,11 +237,11 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen switch await userSession.clientProxy.createRoom(name: state.roomName, topic: state.bindings.roomTopic.isBlank ? nil : state.bindings.roomTopic, - accessType: state.bindings.selectedAccessType, + accessType: state.roomAccessType, isSpace: state.isSpace, userIDs: [], // The invite users screen is shown next so we don't need to invite anyone right now. avatarURL: avatarURL, - aliasLocalPart: state.bindings.selectedAccessType.isPrivate ? nil : state.aliasLocalPart) { + aliasLocalPart: state.roomAccessType.isPrivate ? nil : state.aliasLocalPart) { case .success(let roomID): guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom, diff --git a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift index 86bf2f3df..9f142fd62 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift @@ -39,8 +39,11 @@ struct CreateRoomScreen: View { Form { roomSection topicSection + if context.viewState.canSelectSpace { + selectSpaceSection + } roomAccessSection - if !context.selectedAccessType.isPrivate { + if !context.viewState.roomAccessType.isPrivate { roomAliasSection } } @@ -181,6 +184,7 @@ struct CreateRoomScreen: View { Section { ForEach(context.viewState.availableAccessTypes, id: \.self) { accessType in CreateRoomAccessRow(access: accessType, + spaceName: context.viewState.selectedSpace?.name ?? "", isSelected: context.selectedAccessType == accessType) { context.selectedAccessType = accessType } @@ -215,6 +219,15 @@ struct CreateRoomScreen: View { } } + private var selectSpaceSection: some View { + Section { + EmptyView() + } header: { + Text(L10n.commonSpace) + .compoundListSectionHeader() + } + } + @ToolbarContentBuilder private var toolbar: some ToolbarContent { if context.viewState.shouldShowCancelButton { @@ -237,7 +250,8 @@ struct CreateRoomScreen: View { } private struct CreateRoomAccessRow: View { - let access: CreateRoomAccessType + let access: CreateRoomScreenAccessType + let spaceName: String let isSelected: Bool let onSelection: () -> Void @@ -249,6 +263,10 @@ private struct CreateRoomAccessRow: View { L10n.screenCreateRoomRoomAccessSectionKnockingOptionTitle case .private: L10n.screenCreateRoomRoomAccessSectionPrivateOptionTitle + case .spaceMembers: + L10n.screenCreateRoomRoomAccessSectionRestrictedOptionTitle + case .askToJoinWithSpaceMembers: + L10n.screenCreateRoomRoomAccessSectionKnockingRestrictedOptionTitle } } @@ -260,6 +278,10 @@ private struct CreateRoomAccessRow: View { L10n.screenCreateRoomRoomAccessSectionKnockingOptionDescription case .private: L10n.screenCreateRoomRoomAccessSectionPrivateOptionDescription + case .spaceMembers: + L10n.screenCreateRoomRoomAccessSectionRestrictedOptionDescription(spaceName) + case .askToJoinWithSpaceMembers: + L10n.screenCreateRoomRoomAccessSectionKnockingRestrictedOptionDescription(spaceName) } } @@ -271,6 +293,10 @@ private struct CreateRoomAccessRow: View { \.userAdd case .private: \.lock + case .spaceMembers: + \.space + case .askToJoinWithSpaceMembers: + \.userAdd } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 23aa0e9be..b6e76287e 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -513,7 +513,7 @@ class ClientProxy: ClientProxyProtocol { Self.standardSpaceCreationPowerLevelOverrides } } else { - if accessType == .askToJoin { + if accessType.isAskToJoin { Self.knockingRoomCreationPowerLevelOverrides } else { Self.roomCreationPowerLevelOverrides @@ -1372,7 +1372,7 @@ private extension CreateRoomAccessType { switch self { case .public: false - case .askToJoin, .private: + default: true } } @@ -1393,8 +1393,21 @@ private extension CreateRoomAccessType { switch self { case .askToJoin: .knock + case .spaceMembers(let spaceID): + .restricted(rules: [.roomMembership(roomId: spaceID)]) + case .askToJoinWithSpaceMembers(let spaceID): + .knockRestricted(rules: [.roomMembership(roomId: spaceID)]) case .private, .public: nil } } + + var isAskToJoin: Bool { + switch self { + case .askToJoin, .askToJoinWithSpaceMembers: + true + default: + false + } + } } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index b38bf7616..5e56a0796 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -48,14 +48,16 @@ enum SlidingSyncConstants { static let maximumVisibleRangeSize = 30 } -enum CreateRoomAccessType: CaseIterable { +enum CreateRoomAccessType: Equatable { case `public` + case spaceMembers(spaceID: String) + case askToJoinWithSpaceMembers(spaceID: String) case askToJoin case `private` var isPrivate: Bool { switch self { - case .private: + case .private, .spaceMembers, .askToJoinWithSpaceMembers: true case .public, .askToJoin: false