SP3.1: Update room settings for Spaces element-ios#5231

- Changed the Room Settings screen according to the new design
- Implemented the room access flow
- Added room upgrade support
- Implemented the room suggestion screen
This commit is contained in:
Gil Eluard
2022-01-13 15:53:45 +01:00
parent 085fc7d5b0
commit ce226cff8a
78 changed files with 3755 additions and 196 deletions
@@ -0,0 +1,133 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh RoomAccessCoordinator RoomAccess
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
/// Actions returned by the coordinator callback
enum RoomAccessCoordinatorCoordinatorAction {
case done(String)
case cancel
}
@objcMembers
final class RoomAccessCoordinator: Coordinator {
// MARK: - Properties
// MARK: Private
private let parameters: RoomAccessCoordinatorParameters
private var navigationRouter: NavigationRouterType {
return self.parameters.navigationRouter
}
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((RoomAccessCoordinatorCoordinatorAction) -> Void)?
// MARK: - Setup
init(parameters: RoomAccessCoordinatorParameters) {
self.parameters = parameters
}
// MARK: - Public
func start() {
if #available(iOS 14.0, *) {
MXLog.debug("[RoomAccessCoordinator] did start.")
let rootCoordinator = self.createRoomAccessTypeCoordinator()
rootCoordinator.start()
self.add(childCoordinator: rootCoordinator)
if self.navigationRouter.modules.isEmpty == false {
self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
})
} else {
self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
}
}
}
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private
@available(iOS 14.0, *)
func pushScreen(with coordinator: Coordinator & Presentable) {
add(childCoordinator: coordinator)
self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: coordinator)
})
coordinator.start()
}
@available(iOS 14.0, *)
private func createRoomAccessTypeCoordinator() -> RoomAccessTypeChooserCoordinator {
let coordinator: RoomAccessTypeChooserCoordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, session: parameters.room.mxSession))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .done(let roomId):
self.callback?(.done(roomId))
case .cancel:
self.callback?(.cancel)
case .spaceSelection(let roomId, _):
self.pushScreen(with: self.createRestrictedAccessSpaceChooserCoordinator(with: roomId))
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createRestrictedAccessSpaceChooserCoordinator(with roomId: String) -> MatrixItemChooserCoordinator {
let paramaters = MatrixItemChooserCoordinatorParameters(
session: parameters.room.mxSession,
viewProvider: RoomRestrictedAccessSpaceChooserViewProvider(navTitle: VectorL10n.roomAccessSettingsScreenNavTitle),
itemsProcessor: RoomRestrictedAccessSpaceChooserItemsProcessor(roomId: roomId, session: parameters.room.mxSession))
let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
coordinator.completion = { [weak self] result in
guard let self = self else { return }
switch result {
case .back:
self.navigationRouter.popModule(animated: true)
case .cancel:
self.callback?(.cancel)
case .done:
self.callback?(.done(roomId))
}
}
return coordinator
}
}
@@ -0,0 +1,94 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import UIKit
@objc protocol RoomAccessCoordinatorBridgePresenterDelegate {
func roomAccessCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: RoomAccessCoordinatorBridgePresenter)
func roomAccessCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: RoomAccessCoordinatorBridgePresenter, didCompleteRoomWithId roomId: String)
}
/// RoomNotificationSettingsCoordinatorBridgePresenter enables to start RoomNotificationSettingsCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers).
/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
@objcMembers
final class RoomAccessCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let room: MXRoom
private var coordinator: RoomAccessCoordinator?
// MARK: Public
weak var delegate: RoomAccessCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(room: MXRoom) {
self.room = room
super.init()
}
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
let navigationRouter = NavigationRouter()
let coordinator = RoomAccessCoordinator(parameters: RoomAccessCoordinatorParameters(room: room, navigationRouter: navigationRouter))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .cancel:
self.delegate?.roomAccessCoordinatorBridgePresenterDelegateDidCancel(self)
case .done(let roomId):
self.delegate?.roomAccessCoordinatorBridgePresenterDelegate(self, didCompleteRoomWithId: roomId)
}
}
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
navigationRouter.setRootModule(presentable)
viewController.present(navigationRouter.toPresentable(), animated: animated, completion: nil)
coordinator.start()
self.coordinator = coordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension RoomAccessCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
self.delegate?.roomAccessCoordinatorBridgePresenterDelegateDidCancel(self)
}
}
@@ -0,0 +1,35 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh RoomAccessCoordinator RoomAccess
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// RoomAccessCoordinator input parameters
struct RoomAccessCoordinatorParameters {
/// The Matrix room
let room: MXRoom
/// The navigation router that manage physical navigation
let navigationRouter: NavigationRouterType
init(room: MXRoom,
navigationRouter: NavigationRouterType? = nil) {
self.room = room
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}
@@ -0,0 +1,73 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct RoomAccessTypeChooserCoordinatorParameters {
let roomId: String
let session: MXSession
}
final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: RoomAccessTypeChooserCoordinatorParameters
private let roomAccessTypeChooserHostingController: UIViewController
private var roomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((RoomAccessTypeChooserCoordinatorAction) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init(parameters: RoomAccessTypeChooserCoordinatorParameters) {
self.parameters = parameters
let viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: RoomAccessTypeChooserService(roomId: parameters.roomId, session: parameters.session))
let room = parameters.session.room(withRoomId: parameters.roomId)
let view = RoomAccessTypeChooser(viewModel: viewModel.context, roomName: room?.displayName ?? "")
roomAccessTypeChooserViewModel = viewModel
roomAccessTypeChooserHostingController = VectorHostingController(rootView: view)
}
// MARK: - Public
func start() {
MXLog.debug("[RoomAccessTypeChooserCoordinator] did start.")
roomAccessTypeChooserViewModel.callback = { [weak self] result in
MXLog.debug("[RoomAccessTypeChooserCoordinator] RoomAccessTypeChooserViewModel did complete with result \(result).")
guard let self = self else { return }
switch result {
case .spaceSelection(let roomId, let accessType):
self.callback?(.spaceSelection(roomId, accessType))
case .done(let roomId):
self.callback?(.done(roomId))
case .cancel:
self.callback?(.cancel)
}
}
}
func toPresentable() -> UIViewController {
return self.roomAccessTypeChooserHostingController
}
}
@@ -0,0 +1,59 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case noSelection
case selection
/// The associated screen
var screenType: Any.Type {
RoomAccessTypeChooser.self
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let service: MockRoomAccessTypeChooserService
switch self {
case .noSelection:
service = MockRoomAccessTypeChooserService()
case .selection:
service = MockRoomAccessTypeChooserService(accessItems: [
RoomAccessTypeChooserAccessItem(id: .private, isSelected: true, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: VectorL10n.roomAccessSettingsScreenUpgradeRequired),
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
])
}
let viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: service)
// can simulate service and viewModel actions here if needs be.
return (
[service, viewModel],
AnyView(RoomAccessTypeChooser(viewModel: viewModel.context, roomName: "Room Name")
.addDependency(MockAvatarService.example))
)
}
}
@@ -0,0 +1,79 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
// MARK: - Coordinator
enum RoomAccessTypeChooserAccessType {
case `private`
case restricted
case `public`
}
struct RoomAccessTypeChooserAccessItem: Identifiable, Equatable {
let id: RoomAccessTypeChooserAccessType
var isSelected: Bool
let title: String
let detail: String
var badgeText: String?
}
/// Actions returned by the coordinator callback
enum RoomAccessTypeChooserCoordinatorAction {
case spaceSelection(String, RoomAccessTypeChooserAccessType)
case done(String)
case cancel
}
// MARK: - View model
/// Actions to be performed on the `ViewModel` State
enum RoomAccessTypeChooserStateAction {
case updateAccessItems([RoomAccessTypeChooserAccessItem])
case updateShowUpgradeRoomAlert(Bool)
case updateWaitingMessage(String?)
}
/// Actions sent by the`ViewModel` to the `Coordinator`.
enum RoomAccessTypeChooserViewModelAction {
case spaceSelection(String, RoomAccessTypeChooserAccessType)
case done(String)
case cancel
}
// MARK: - View
/// State managed by the `ViewModel` delivered to the `View`.
struct RoomAccessTypeChooserViewState: BindableState {
var accessItems: [RoomAccessTypeChooserAccessItem]
var bindings: RoomAccessTypeChooserViewModelBindings
}
struct RoomAccessTypeChooserViewModelBindings {
var showUpgradeRoomAlert: Bool
var waitingMessage: String?
var isLoading: Bool
}
/// Actions send from the `View` to the `ViewModel`.
enum RoomAccessTypeChooserViewAction {
case cancel
case done
case didSelectAccessType(RoomAccessTypeChooserAccessType)
case didCancelRoomUpgrade
case didAcceptRoomUpgrade(Bool)
}
@@ -0,0 +1,124 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
import Combine
@available(iOS 14, *)
typealias RoomAccessTypeChooserViewModelType = StateStoreViewModel<RoomAccessTypeChooserViewState,
RoomAccessTypeChooserStateAction,
RoomAccessTypeChooserViewAction>
@available(iOS 14.0, *)
class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAccessTypeChooserViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let roomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol
// MARK: Public
var callback: ((RoomAccessTypeChooserViewModelAction) -> Void)?
// MARK: - Setup
init(roomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol) {
self.roomAccessTypeChooserService = roomAccessTypeChooserService
super.init(initialViewState: Self.defaultState(roomAccessTypeChooserService: roomAccessTypeChooserService))
startObservingService()
}
private static func defaultState(roomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol) -> RoomAccessTypeChooserViewState {
let bindings = RoomAccessTypeChooserViewModelBindings(
showUpgradeRoomAlert: roomAccessTypeChooserService.roomUpgradeRequiredSubject.value,
waitingMessage: roomAccessTypeChooserService.waitingMessageSubject.value, isLoading: roomAccessTypeChooserService.waitingMessageSubject.value != nil)
return RoomAccessTypeChooserViewState(accessItems: roomAccessTypeChooserService.accessItemsSubject.value, bindings: bindings)
}
private func startObservingService() {
let accessTypePublisher = roomAccessTypeChooserService.accessItemsSubject
.map(RoomAccessTypeChooserStateAction.updateAccessItems)
.eraseToAnyPublisher()
dispatch(actionPublisher: accessTypePublisher)
let showUpgradeRoomAlertPublisher = roomAccessTypeChooserService.roomUpgradeRequiredSubject
.map(RoomAccessTypeChooserStateAction.updateShowUpgradeRoomAlert)
.eraseToAnyPublisher()
dispatch(actionPublisher: showUpgradeRoomAlertPublisher)
let waitingMessagePublisher = roomAccessTypeChooserService.waitingMessageSubject
.map(RoomAccessTypeChooserStateAction.updateWaitingMessage)
.eraseToAnyPublisher()
dispatch(actionPublisher: waitingMessagePublisher)
}
// MARK: - Public
override func process(viewAction: RoomAccessTypeChooserViewAction) {
switch viewAction {
case .didSelectAccessType(let accessType):
didSelect(accessType: accessType)
case .done:
done()
case .cancel:
cancel()
case .didCancelRoomUpgrade:
roomAccessTypeChooserService.upgradeRoom(accepted: false, autoInviteUsers: false) { _, _ in }
case .didAcceptRoomUpgrade(let autoInviteUsers):
roomAccessTypeChooserService.upgradeRoom(accepted: true, autoInviteUsers: autoInviteUsers) { [weak self] upgradeFinished, roomId in
if upgradeFinished {
self?.callback?(.spaceSelection(roomId, .restricted))
}
}
}
}
override class func reducer(state: inout RoomAccessTypeChooserViewState, action: RoomAccessTypeChooserStateAction) {
switch action {
case .updateAccessItems(let accessItems):
state.accessItems = accessItems
case .updateShowUpgradeRoomAlert(let show):
state.bindings.showUpgradeRoomAlert = show
case .updateWaitingMessage(let message):
state.bindings.waitingMessage = message
state.bindings.isLoading = message != nil
}
}
// MARK: - Private
private func done() {
roomAccessTypeChooserService.applySelection { [weak self] in
guard let self = self else { return }
if self.roomAccessTypeChooserService.selectedType == .restricted {
self.callback?(.spaceSelection(self.roomAccessTypeChooserService.currentRoomId, .restricted))
} else {
self.callback?(.done(self.roomAccessTypeChooserService.currentRoomId))
}
}
}
private func cancel() {
callback?(.cancel)
}
private func didSelect(accessType: RoomAccessTypeChooserAccessType) {
roomAccessTypeChooserService.updateSelection(with: accessType)
if accessType == .restricted && !roomAccessTypeChooserService.roomUpgradeRequiredSubject.value {
callback?(.spaceSelection(roomAccessTypeChooserService.currentRoomId, .restricted))
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol RoomAccessTypeChooserViewModelProtocol {
var callback: ((RoomAccessTypeChooserViewModelAction) -> Void)? { get set }
@available(iOS 14, *)
var context: RoomAccessTypeChooserViewModelType.Context { get }
}
@@ -0,0 +1,299 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import Combine
@available(iOS 14.0, *)
class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
// MARK: - Properties
// MARK: Private
private let roomId: String
private let session:MXSession
private var replacementRoom: MXRoom?
private var didBuildSpaceGraphObserver: Any?
private var accessItems: [RoomAccessTypeChooserAccessItem] = []
private var roomUpgradeRequired = false {
didSet {
for (index, item) in accessItems.enumerated() {
if item.id == .restricted {
accessItems[index].badgeText = roomUpgradeRequired ? VectorL10n.roomAccessSettingsScreenUpgradeRequired : VectorL10n.roomAccessSettingsScreenEditSpaces
}
}
accessItemsSubject.send(accessItems)
}
}
private(set) var selectedType: RoomAccessTypeChooserAccessType = .private {
didSet {
for (index, item) in accessItems.enumerated() {
accessItems[index].isSelected = selectedType == item.id
}
accessItemsSubject.send(accessItems)
}
}
private var roomJoinRule: MXRoomJoinRule = .private
private var currentOperation: MXHTTPOperation?
private let restrictedVersionOverride: String?
// MARK: Public
private(set) var accessItemsSubject: CurrentValueSubject<[RoomAccessTypeChooserAccessItem], Never>
private(set) var roomUpgradeRequiredSubject: CurrentValueSubject<Bool, Never>
private(set) var waitingMessageSubject: CurrentValueSubject<String?, Never>
private(set) var errorSubject: CurrentValueSubject<Error?, Never>
private(set) var currentRoomId: String
// MARK: - Setup
init(roomId: String, session: MXSession) {
self.roomId = roomId
self.session = session
self.currentRoomId = roomId
restrictedVersionOverride = session.homeserverCapabilities.versionOverrideForFeature(.restricted)
roomUpgradeRequiredSubject = CurrentValueSubject(false)
waitingMessageSubject = CurrentValueSubject(nil)
accessItemsSubject = CurrentValueSubject(accessItems)
errorSubject = CurrentValueSubject(nil)
setupAccessItems()
readRoomState()
}
deinit {
currentOperation?.cancel()
if let observer = self.didBuildSpaceGraphObserver {
NotificationCenter.default.removeObserver(observer)
}
}
func updateSelection(with selectedType: RoomAccessTypeChooserAccessType) {
self.selectedType = selectedType
if selectedType == .restricted {
if roomUpgradeRequired && roomUpgradeRequiredSubject.value == false {
roomUpgradeRequiredSubject.send(true)
}
}
}
func applySelection(completion: @escaping () -> Void) {
guard let room = session.room(withRoomId: currentRoomId) else {
fatalError("[RoomAccessTypeChooserService] applySelection: room with ID \(currentRoomId) not found")
}
let _joinRule: MXRoomJoinRule?
switch self.selectedType {
case .private:
_joinRule = .private
case .public:
_joinRule = .public
case .restricted:
_joinRule = nil
if roomUpgradeRequired && roomUpgradeRequiredSubject.value == false {
roomUpgradeRequiredSubject.send(true)
} else {
completion()
}
}
if let joinRule = _joinRule {
// waitingMessageSubject.send(VectorL10n.roomAccessSettingsScreenSettingRoomAccess)
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// self.waitingMessageSubject.send(nil)
// completion()
// }
room.setJoinRule(joinRule) { [weak self] response in
guard let self = self else { return }
self.waitingMessageSubject.send(nil)
switch response {
case .failure(let error):
self.errorSubject.send(error)
case .success:
completion()
}
}
}
}
func upgradeRoom(accepted: Bool, autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) {
roomUpgradeRequiredSubject.send(false)
guard let restrictedVersionOverride = restrictedVersionOverride, accepted else {
setupDefaultSelectionType()
completion(false, currentRoomId)
return
}
waitingMessageSubject.send(VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgrading)
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// self.waitingMessageSubject.send(nil)
// self.roomUpgradeRequired = false
// completion(true, self.currentRoomId)
// }
if autoInviteUsers, let room = session.room(withRoomId: self.currentRoomId) {
self.currentOperation = room.members { [weak self] response in
guard let self = self else { return }
switch response {
case .success(let members):
let memberIds: [String] = members?.members.compactMap({ member in
guard member.membership == .join, member.userId != self.session.myUserId else {
return nil
}
return member.userId
}) ?? []
self.upgradeRoom(to: restrictedVersionOverride, inviteUsers: memberIds, completion: completion)
case .failure(let error):
self.waitingMessageSubject.send(nil)
self.errorSubject.send(error)
}
}
} else {
self.upgradeRoom(to: restrictedVersionOverride, inviteUsers: [], completion: completion)
}
}
// MARK: - Private
private func setupAccessItems() {
guard let spaceService = session.spaceService, let ancestors = spaceService.ancestorsPerRoomId[currentRoomId], !ancestors.isEmpty else {
self.accessItems = [
RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
]
return
}
self.accessItems = [
RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: roomUpgradeRequired ? VectorL10n.roomAccessSettingsScreenUpgradeRequired : VectorL10n.roomAccessSettingsScreenEditSpaces),
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
]
accessItemsSubject.send(accessItems)
}
private func readRoomState() {
guard let room = session.room(withRoomId: currentRoomId) else {
fatalError("[RoomAccessTypeChooserService] readRoomState: room with ID \(currentRoomId) not found")
}
room.state { [weak self] state in
guard let self = self else { return }
if let roomVersion = state?.stateEvents(with: .roomCreate)?.last?.wireContent["room_version"] as? String, let homeserverCapabilitiesService = self.session.homeserverCapabilities {
self.roomUpgradeRequired = self.restrictedVersionOverride != nil && !homeserverCapabilitiesService.isFeatureSupported(.restricted, by: roomVersion)
}
self.roomJoinRule = state?.joinRule ?? .private
self.setupDefaultSelectionType()
}
}
private func setupDefaultSelectionType() {
switch roomJoinRule {
case .restricted:
selectedType = .restricted
case .public:
selectedType = .public
default:
selectedType = .private
}
}
private func upgradeRoom(to restrictedVersionOverride: String, inviteUsers userIds: [String], completion: @escaping (Bool, String) -> Void) {
currentOperation = session.matrixRestClient.upgradeRoom(withId: self.currentRoomId, to: restrictedVersionOverride) { [weak self] response in
guard let self = self else { return }
switch response {
case .success(let replacementRoomId):
let oldRoomId = self.currentRoomId
self.roomUpgradeRequired = false
self.currentRoomId = replacementRoomId
let parentSpaces = self.session.spaceService.directParentIds(ofRoomWithId: oldRoomId)
self.moveRoom(from: oldRoomId, to: replacementRoomId, within: Array(parentSpaces), at: 0) {
self.didBuildSpaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] notification in
guard let self = self else { return }
if let observer = self.didBuildSpaceGraphObserver {
NotificationCenter.default.removeObserver(observer)
self.didBuildSpaceGraphObserver = nil
}
DispatchQueue.main.async {
self.inviteUser(from: userIds, at: 0, completion: completion)
}
}
}
case .failure(let error):
self.waitingMessageSubject.send(nil)
self.errorSubject.send(error)
}
}
}
private func moveRoom(from roomId: String, to newRoomId: String, within parentIds: [String], at index: Int, completion: @escaping () -> Void) {
guard index < parentIds.count else {
completion()
return
}
guard let space = session.spaceService.getSpace(withId: parentIds[index]) else {
MXLog.warning("[RoomAccessTypeChooserService] moveRoom \(roomId) to \(newRoomId) within \(parentIds[index]): space not found")
moveRoom(from: roomId, to: newRoomId, within: parentIds, at: index + 1, completion: completion)
return
}
space.moveChild(withRoomId: roomId, to: newRoomId) { [weak self] response in
guard let self = self else { return }
if let error = response.error {
MXLog.warning("[RoomAccessTypeChooserService] moveRoom \(roomId) to \(newRoomId) within \(space.spaceId): failed due to error: \(error)")
}
self.moveRoom(from: roomId, to: newRoomId, within: parentIds, at: index + 1, completion: completion)
}
}
private func inviteUser(from userIds: [String], at index: Int, completion: @escaping (Bool, String) -> Void) {
guard index < userIds.count else {
self.waitingMessageSubject.send(nil)
completion(true, currentRoomId)
return
}
currentOperation = session.matrixRestClient.invite(.userId(userIds[index]), toRoom: currentRoomId) { [weak self] response in
guard let self = self else { return }
self.currentOperation = nil
if let error = response.error {
MXLog.warning("[RoomAccessTypeChooserService] inviteUser: failed to invite \(userIds[index]) to \(self.currentRoomId) due to error: \(error)")
}
self.inviteUser(from: userIds, at: index + 1, completion: completion)
}
}
}
@@ -0,0 +1,61 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import Combine
@available(iOS 14.0, *)
class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
static let mockAccessItems: [RoomAccessTypeChooserAccessItem] = [
RoomAccessTypeChooserAccessItem(id: .private, isSelected: true, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: VectorL10n.roomAccessSettingsScreenUpgradeRequired),
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
]
private(set) var accessItemsSubject: CurrentValueSubject<[RoomAccessTypeChooserAccessItem], Never>
private(set) var roomUpgradeRequiredSubject: CurrentValueSubject<Bool, Never>
private(set) var waitingMessageSubject: CurrentValueSubject<String?, Never>
private(set) var errorSubject: CurrentValueSubject<Error?, Never>
private(set) var selectedType: RoomAccessTypeChooserAccessType = .private
var currentRoomId: String {
return "sldkfjsdljf:,atrix.org"
}
init(accessItems: [RoomAccessTypeChooserAccessItem] = mockAccessItems) {
accessItemsSubject = CurrentValueSubject(accessItems)
roomUpgradeRequiredSubject = CurrentValueSubject(false)
waitingMessageSubject = CurrentValueSubject(nil)
errorSubject = CurrentValueSubject(nil)
}
func simulateUpdate(accessItems: [RoomAccessTypeChooserAccessItem]) {
self.accessItemsSubject.send(accessItems)
}
func updateSelection(with selectedType: RoomAccessTypeChooserAccessType) {
}
func upgradeRoom(accepted: Bool, autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) {
}
func applySelection(completion: @escaping () -> Void) {
}
}
@@ -0,0 +1,33 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import Combine
@available(iOS 14.0, *)
protocol RoomAccessTypeChooserServiceProtocol {
var accessItemsSubject: CurrentValueSubject<[RoomAccessTypeChooserAccessItem], Never> { get }
var roomUpgradeRequiredSubject: CurrentValueSubject<Bool, Never> { get }
var waitingMessageSubject: CurrentValueSubject<String?, Never> { get }
var errorSubject: CurrentValueSubject<Error?, Never> { get }
var selectedType: RoomAccessTypeChooserAccessType { get }
var currentRoomId: String { get }
func updateSelection(with selectedType: RoomAccessTypeChooserAccessType)
func applySelection(completion: @escaping () -> Void)
func upgradeRoom(accepted: Bool, autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void)
}
@@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class RoomAccessTypeChooserUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockRoomAccessTypeChooserScreenState.self
}
override class func createTest() -> MockScreenTest {
return RoomAccessTypeChooserUITests(selector: #selector(verifyRoomAccessTypeChooserScreen))
}
func verifyRoomAccessTypeChooserScreen() throws {
guard let screenState = screenState as? MockRoomAccessTypeChooserScreenState else { fatalError("no screen") }
switch screenState {
case .noRooms:
verifyRoomAccessTypeChooserNoRooms()
case .rooms:
verifyRoomAccessTypeChooserRooms()
}
}
func verifyRoomAccessTypeChooserNoRooms() {
let errorMessage = app.staticTexts["errorMessage"]
XCTAssert(errorMessage.exists)
XCTAssert(errorMessage.label == "No Rooms")
}
func verifyRoomAccessTypeChooserRooms() {
let displayNameCount = app.buttons.matching(identifier:"roomNameText").count
XCTAssertEqual(displayNameCount, 3)
}
}
@@ -0,0 +1,53 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import Combine
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class RoomAccessTypeChooserViewModelTests: XCTestCase {
private enum Constants {
}
var service: MockRoomAccessTypeChooserService!
var viewModel: RoomAccessTypeChooserViewModel!
var context: RoomAccessTypeChooserViewModel.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
service = MockRoomAccessTypeChooserService()
viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: service)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.rooms, MockRoomAccessTypeChooserService.mockRooms)
}
func testFirstValueReceived() throws {
let roomsPublisher = context.$viewState.map(\.rooms).removeDuplicates().collect(1).first()
XCTAssertEqual(try xcAwait(roomsPublisher), [MockRoomAccessTypeChooserService.mockRooms])
}
func testUpdatesReceived() throws {
let updatedRooms = Array(MockRoomAccessTypeChooserService.mockRooms.dropLast())
let roomsPublisher = context.$viewState.map(\.rooms).removeDuplicates().collect(2).first()
let awaitDeferred = xcAwaitDeferred(roomsPublisher)
service.simulateUpdate(rooms: updatedRooms)
XCTAssertEqual(try awaitDeferred(), [MockRoomAccessTypeChooserService.mockRooms, updatedRooms])
}
}
@@ -0,0 +1,104 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct RoomAccessTypeChooser: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ObservedObject var viewModel: RoomAccessTypeChooserViewModelType.Context
let roomName: String
var body: some View {
listContent
.modifier(WaitOverlay(
allowUserInteraction: false,
message: $viewModel.waitingMessage,
isLoading: $viewModel.isLoading))
.modal(withStyle: .overFullScreen,
modalTransitionStyle: .crossDissolve,
id: "RoomAccessTypeChooser-RoomAccessTypeChooserUpgradeRoomAlert",
isPresented: $viewModel.showUpgradeRoomAlert) {
RoomAccessTypeChooserUpgradeRoomAlert(viewModel: viewModel)
}
.navigationTitle(VectorL10n.roomAccessSettingsScreenNavTitle)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(VectorL10n.cancel) {
viewModel.send(viewAction: .cancel)
}
.disabled(viewModel.isLoading)
}
ToolbarItem(placement: .confirmationAction) {
Button(VectorL10n.done) {
viewModel.send(viewAction: .done)
}
.disabled(viewModel.isLoading)
}
}
}
// MARK: Private
@ViewBuilder
private var listContent: some View {
ScrollView{
VStack(alignment: .leading) {
Text(VectorL10n.roomAccessSettingsScreenTitle)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.bodySB)
.padding(.top, 24)
Text(VectorL10n.roomAccessSettingsScreenMessage(roomName))
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.callout)
.padding(.top, 8)
LazyVStack(spacing: 0) {
ForEach(viewModel.viewState.accessItems) { item in
RoomAccessTypeChooserRow(isSelected: item.isSelected, title: item.title, message: item.detail, badgeText: item.badgeText)
.onTapGesture {
viewModel.send(viewAction: .didSelectAccessType(item.id))
}
}
}
.frame(maxHeight: .infinity, alignment: .top)
.padding(.top, 30)
}
.padding(.horizontal)
}.background(theme.colors.background)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct RoomAccessTypeChooser_Previews: PreviewProvider {
static let stateRenderer = MockRoomAccessTypeChooserScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup(addNavigation: true)
.theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true)
.theme(.dark).preferredColorScheme(.dark)
}
}
@@ -0,0 +1,95 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct RoomAccessTypeChooserRow: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
let isSelected: Bool
let title: String
let message: String
let badgeText: String?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.body)
Text(message)
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.subheadline)
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill")
.renderingMode(.template)
.foregroundColor(theme.colors.accent)
} else {
Image(systemName: "circle")
.renderingMode(.template)
.foregroundColor(theme.colors.quarterlyContent)
}
}
if let badgeText = badgeText {
Text(badgeText)
.foregroundColor(theme.colors.accent)
.font(theme.fonts.callout)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder()
.foregroundColor(theme.colors.accent)
)
}
Divider().background(theme.colors.quinaryContent)
}
.background(theme.colors.background)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct RoomAccessTypeChooserRow_Previews: PreviewProvider {
static var previews: some View {
VStack {
RoomAccessTypeChooserRow(isSelected: false, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: nil)
RoomAccessTypeChooserRow(isSelected: false, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: "Badge Text")
RoomAccessTypeChooserRow(isSelected: true, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: nil)
RoomAccessTypeChooserRow(isSelected: true, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: "Badge Text")
}.theme(.light).preferredColorScheme(.light)
VStack {
RoomAccessTypeChooserRow(isSelected: false, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: nil)
RoomAccessTypeChooserRow(isSelected: false, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: "Badge Text")
RoomAccessTypeChooserRow(isSelected: true, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: nil)
RoomAccessTypeChooserRow(isSelected: true, title: "Title of the row", message: "Some very long message just to figure out if the row behaves as expected", badgeText: "Badge Text")
}.theme(.dark).preferredColorScheme(.dark)
}
}
@@ -0,0 +1,94 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct RoomAccessTypeChooserUpgradeRoomAlert: View {
// MARK: - Properties
@ObservedObject var viewModel: RoomAccessTypeChooserViewModelType.Context
@State var autoInviteUsers: Bool = true
// MARK: - Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: - Public
var body: some View {
ZStack {
Color.black.opacity(0.6)
alertContent
.modifier(WaitOverlay(
allowUserInteraction: false,
message: $viewModel.waitingMessage,
isLoading: $viewModel.isLoading))
}
.edgesIgnoringSafeArea(.all)
}
// MARK: - Private
@ViewBuilder
private var alertContent: some View {
VStack(alignment: .center) {
Text(VectorL10n.roomAccessSettingsScreenUpgradeAlertTitle)
.font(theme.fonts.title3SB)
.foregroundColor(theme.colors.primaryContent)
.padding(.top, 16)
.padding(.bottom, 24)
Text(VectorL10n.roomAccessSettingsScreenUpgradeAlertMessage)
.multilineTextAlignment(.center)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.secondaryContent)
.padding(.bottom, 35)
.padding(.horizontal, 12)
Toggle(isOn: $autoInviteUsers, label: {
Text(VectorL10n.roomAccessSettingsScreenUpgradeAlertAutoInviteSwitch)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
})
.toggleStyle(SwitchToggleStyle(tint: theme.colors.accent))
.padding(.horizontal, 28)
Divider()
.padding(.horizontal, 28)
Button {
viewModel.send(viewAction: .didAcceptRoomUpgrade(autoInviteUsers))
} label: {
Text(VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgradeButton)
}
.buttonStyle(PrimaryActionButtonStyle())
.accessibilityIdentifier("upgradeButton")
.padding(.horizontal, 24)
.padding(.top, 16)
Button {
viewModel.send(viewAction: .didCancelRoomUpgrade)
} label: {
Text(VectorL10n.cancel)
}
.buttonStyle(SecondaryActionButtonStyle())
.accessibilityIdentifier("cancelButton")
.padding(.horizontal, 24)
.padding(.vertical, 16)
}
.background(RoundedRectangle.init(cornerRadius: 8).foregroundColor(theme.colors.background))
.padding(.horizontal, 20)
.frame(minWidth: 0, maxWidth: 500)
}
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
class RoomRestrictedAccessSpaceChooserViewProvider: MatrixItemChooserCoordinatorViewProvider {
private let navTitle: String?
init(navTitle: String?) {
self.navTitle = navTitle
}
@available(iOS 14, *)
func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView {
return AnyView(RoomRestrictedAccessSpaceChooserSelector(viewModel: viewModel, navTitle: navTitle))
}
}
@@ -0,0 +1,60 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
class RoomRestrictedAccessSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtocol {
// MARK: Private
private let roomId: String
private let session: MXSession
// MARK: Setup
init(roomId: String, session: MXSession) {
self.roomId = roomId
self.session = session
self.dataSource = MatrixItemChooserRoomRestrictedAllowedParentsDataSource(roomId: roomId)
}
// MARK: MatrixItemChooserSelectionProcessorProtocol
private(set) var dataSource: MatrixItemChooserDataSource
var loadingText: String? {
VectorL10n.roomAccessSettingsScreenSettingRoomAccess
}
func computeSelection(withIds itemsIds: [String], completion: @escaping (Result<Void, Error>) -> Void) {
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// completion(.success(()))
// }
session.matrixRestClient.setRoomJoinRule(.restricted, forRoomWithId: roomId, allowedParentIds: itemsIds) { response in
switch response {
case .failure(let error):
completion(.failure(error))
case .success:
completion(.success(()))
}
}
}
func isItemIncluded(_ item: (MatrixListItemData)) -> Bool {
return true
}
}
@@ -0,0 +1,53 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct RoomRestrictedAccessSpaceChooserSelector: View {
// MARK: Properties
@ObservedObject var viewModel: MatrixItemChooserViewModel.Context
let navTitle: String?
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
@ViewBuilder
var body: some View {
MatrixItemChooser(viewModel: viewModel)
.background(theme.colors.background)
.navigationTitle(VectorL10n.roomAccessSettingsScreenNavTitle)
// .navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(VectorL10n.cancel) {
viewModel.send(viewAction: .cancel)
}
.disabled(viewModel.viewState.loading)
}
ToolbarItem(placement: .confirmationAction) {
Button(VectorL10n.done) {
viewModel.send(viewAction: .done)
}
.disabled(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading)
}
}
}
}