mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-22 01:22:46 +02:00
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:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+94
@@ -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())
|
||||
}
|
||||
}
|
||||
+73
@@ -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
|
||||
}
|
||||
}
|
||||
+59
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
+79
@@ -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)
|
||||
}
|
||||
+124
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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 }
|
||||
}
|
||||
+299
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
+33
@@ -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)
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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])
|
||||
}
|
||||
}
|
||||
+104
@@ -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)
|
||||
}
|
||||
}
|
||||
+95
@@ -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)
|
||||
}
|
||||
}
|
||||
+94
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -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))
|
||||
}
|
||||
}
|
||||
+60
@@ -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
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user