mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-25 11:02:48 +02:00
@@ -25,6 +25,7 @@ enum RoomAccessCoordinatorCoordinatorAction {
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@available(iOS 14.0, *)
|
||||
final class RoomAccessCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -52,6 +53,8 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
return parameters.room.roomId
|
||||
}
|
||||
|
||||
private weak var accessCoordinator: RoomAccessTypeChooserCoordinator?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: RoomAccessCoordinatorParameters) {
|
||||
@@ -62,21 +65,20 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
MXLog.debug("[RoomAccessCoordinator] did start.")
|
||||
let rootCoordinator = self.createRoomAccessTypeCoordinator()
|
||||
rootCoordinator.start()
|
||||
|
||||
self.add(childCoordinator: rootCoordinator)
|
||||
self.accessCoordinator = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +89,6 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func pushScreen(with coordinator: Coordinator & Presentable) {
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
@@ -98,7 +99,16 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func popupScreen(with coordinator: Coordinator & Presentable) {
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
coordinator.toPresentable().modalPresentationStyle = .overFullScreen
|
||||
coordinator.toPresentable().modalTransitionStyle = .crossDissolve
|
||||
self.navigationRouter.present(coordinator, animated: true)
|
||||
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func createRoomAccessTypeCoordinator() -> RoomAccessTypeChooserCoordinator {
|
||||
let coordinator: RoomAccessTypeChooserCoordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, session: parameters.room.mxSession))
|
||||
coordinator.callback = { [weak self] result in
|
||||
@@ -112,12 +122,13 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
case .spaceSelection(let roomId, _):
|
||||
self.upgradedRoomId = roomId
|
||||
self.pushScreen(with: self.createRestrictedAccessSpaceChooserCoordinator(with: roomId))
|
||||
case .roomUpgradeNeeded(let roomId, let versionOverride):
|
||||
self.popupScreen(with: self.createUpgradeRoomCoordinator(withRoomWithId: roomId, to: versionOverride))
|
||||
}
|
||||
}
|
||||
return coordinator
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private func createRestrictedAccessSpaceChooserCoordinator(with roomId: String) -> MatrixItemChooserCoordinator {
|
||||
let paramaters = MatrixItemChooserCoordinatorParameters(
|
||||
session: parameters.room.mxSession,
|
||||
@@ -139,4 +150,23 @@ final class RoomAccessCoordinator: Coordinator {
|
||||
return coordinator
|
||||
}
|
||||
|
||||
private func createUpgradeRoomCoordinator(withRoomWithId roomId: String, to versionOverride: String) -> RoomUpgradeCoordinator {
|
||||
let paramaters = RoomUpgradeCoordinatorParameters(
|
||||
session: parameters.room.mxSession,
|
||||
roomId: roomId,
|
||||
versionOverride: versionOverride)
|
||||
let coordinator = RoomUpgradeCoordinator(parameters: paramaters)
|
||||
|
||||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.accessCoordinator?.handleRoomUpgradeResult(result)
|
||||
|
||||
self.navigationRouter.dismissModule(animated: true) {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
return coordinator
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -15,6 +15,7 @@
|
||||
//
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
@objc protocol RoomAccessCoordinatorBridgePresenterDelegate {
|
||||
func roomAccessCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: RoomAccessCoordinatorBridgePresenter, didCancelRoomWithId roomId: String)
|
||||
func roomAccessCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: RoomAccessCoordinatorBridgePresenter, didCompleteRoomWithId roomId: String)
|
||||
@@ -25,6 +26,7 @@ import UIKit
|
||||
/// 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
|
||||
@available(iOS 14.0, *)
|
||||
final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -85,6 +87,7 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension RoomAccessCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
|
||||
|
||||
+6
@@ -63,6 +63,8 @@ final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable {
|
||||
self.callback?(.done(roomId))
|
||||
case .cancel(let roomId):
|
||||
self.callback?(.cancel(roomId))
|
||||
case .roomUpgradeNeeded(let roomId, let versionOverride):
|
||||
self.callback?(.roomUpgradeNeeded(roomId, versionOverride))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,4 +72,8 @@ final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable {
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.roomAccessTypeChooserHostingController
|
||||
}
|
||||
|
||||
func handleRoomUpgradeResult(_ result: RoomUpgradeCoordinatorResult) {
|
||||
self.roomAccessTypeChooserViewModel.handleRoomUpgradeResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,6 +35,7 @@ struct RoomAccessTypeChooserAccessItem: Identifiable, Equatable {
|
||||
/// Actions returned by the coordinator callback
|
||||
enum RoomAccessTypeChooserCoordinatorAction {
|
||||
case spaceSelection(String, RoomAccessTypeChooserAccessType)
|
||||
case roomUpgradeNeeded(String, String)
|
||||
case done(String)
|
||||
case cancel(String)
|
||||
}
|
||||
@@ -51,6 +52,7 @@ enum RoomAccessTypeChooserStateAction {
|
||||
/// Actions sent by the`ViewModel` to the `Coordinator`.
|
||||
enum RoomAccessTypeChooserViewModelAction {
|
||||
case spaceSelection(String, RoomAccessTypeChooserAccessType)
|
||||
case roomUpgradeNeeded(String, String)
|
||||
case done(String)
|
||||
case cancel(String)
|
||||
}
|
||||
@@ -74,6 +76,4 @@ enum RoomAccessTypeChooserViewAction {
|
||||
case cancel
|
||||
case done
|
||||
case didSelectAccessType(RoomAccessTypeChooserAccessType)
|
||||
case didCancelRoomUpgrade
|
||||
case didAcceptRoomUpgrade(Bool)
|
||||
}
|
||||
|
||||
+27
-12
@@ -54,10 +54,14 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc
|
||||
.map(RoomAccessTypeChooserStateAction.updateAccessItems)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: accessTypePublisher)
|
||||
let showUpgradeRoomAlertPublisher = roomAccessTypeChooserService.roomUpgradeRequiredSubject
|
||||
.map(RoomAccessTypeChooserStateAction.updateShowUpgradeRoomAlert)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: showUpgradeRoomAlertPublisher)
|
||||
roomAccessTypeChooserService
|
||||
.roomUpgradeRequiredSubject
|
||||
.sink { [weak self] isUpgradeRequired in
|
||||
if isUpgradeRequired {
|
||||
self?.upgradeRoom()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
let waitingMessagePublisher = roomAccessTypeChooserService.waitingMessageSubject
|
||||
.map(RoomAccessTypeChooserStateAction.updateWaitingMessage)
|
||||
.eraseToAnyPublisher()
|
||||
@@ -74,14 +78,6 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +93,27 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoomUpgradeResult(_ result: RoomUpgradeCoordinatorResult) {
|
||||
switch result {
|
||||
case .cancel(let roomId):
|
||||
roomAccessTypeChooserService.updateRoomId(with: roomId)
|
||||
case .done(let roomId):
|
||||
roomAccessTypeChooserService.updateRoomId(with: roomId)
|
||||
callback?(.spaceSelection(roomId, .restricted))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func upgradeRoom() {
|
||||
guard let versionOverride = roomAccessTypeChooserService.versionOverride else {
|
||||
MXLog.error("[RoomAccessTypeChooserViewModel] upgradeRoom: versionOverride not found")
|
||||
return
|
||||
}
|
||||
|
||||
callback?(.roomUpgradeNeeded(roomAccessTypeChooserService.currentRoomId, versionOverride))
|
||||
}
|
||||
|
||||
private func done() {
|
||||
roomAccessTypeChooserService.applySelection { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
+2
@@ -20,4 +20,6 @@ protocol RoomAccessTypeChooserViewModelProtocol {
|
||||
var callback: ((RoomAccessTypeChooserViewModelAction) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
var context: RoomAccessTypeChooserViewModelType.Context { get }
|
||||
|
||||
func handleRoomUpgradeResult(_ result: RoomUpgradeCoordinatorResult)
|
||||
}
|
||||
|
||||
+16
-113
@@ -50,7 +50,6 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
}
|
||||
private var roomJoinRule: MXRoomJoinRule = .private
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
private let restrictedVersionOverride: String?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -60,6 +59,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
private(set) var errorSubject: CurrentValueSubject<Error?, Never>
|
||||
|
||||
private(set) var currentRoomId: String
|
||||
private(set) var versionOverride: String?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -67,7 +67,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
self.roomId = roomId
|
||||
self.session = session
|
||||
self.currentRoomId = roomId
|
||||
restrictedVersionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
|
||||
self.versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
|
||||
|
||||
roomUpgradeRequiredSubject = CurrentValueSubject(false)
|
||||
waitingMessageSubject = CurrentValueSubject(nil)
|
||||
@@ -85,6 +85,8 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func updateSelection(with selectedType: RoomAccessTypeChooserAccessType) {
|
||||
self.selectedType = selectedType
|
||||
|
||||
@@ -97,7 +99,8 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
|
||||
func applySelection(completion: @escaping () -> Void) {
|
||||
guard let room = session.room(withRoomId: currentRoomId) else {
|
||||
fatalError("[RoomAccessTypeChooserService] applySelection: room with ID \(currentRoomId) not found")
|
||||
MXLog.error("[RoomAccessTypeChooserService] applySelection: room with ID \(currentRoomId) not found")
|
||||
return
|
||||
}
|
||||
|
||||
let _joinRule: MXRoomJoinRule?
|
||||
@@ -117,6 +120,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
}
|
||||
|
||||
if let joinRule = _joinRule {
|
||||
self.waitingMessageSubject.send(VectorL10n.roomAccessSettingsScreenSettingRoomAccess)
|
||||
|
||||
room.setJoinRule(joinRule) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
@@ -132,38 +136,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
func updateRoomId(with roomId: String) {
|
||||
self.currentRoomId = roomId
|
||||
readRoomState()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -188,21 +163,22 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
|
||||
private func readRoomState() {
|
||||
guard let room = session.room(withRoomId: currentRoomId) else {
|
||||
fatalError("[RoomAccessTypeChooserService] readRoomState: room with ID \(currentRoomId) not found")
|
||||
MXLog.error("[RoomAccessTypeChooserService] readRoomState: room with ID \(currentRoomId) not found")
|
||||
return
|
||||
}
|
||||
|
||||
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.homeserverCapabilitiesService {
|
||||
self.roomUpgradeRequired = self.restrictedVersionOverride != nil && !homeserverCapabilitiesService.isFeatureSupported(.restricted, by: roomVersion)
|
||||
self.roomUpgradeRequired = self.versionOverride != nil && !homeserverCapabilitiesService.isFeatureSupported(.restricted, by: roomVersion)
|
||||
}
|
||||
|
||||
self.roomJoinRule = state?.joinRule ?? .private
|
||||
self.setupDefaultSelectionType()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func setupDefaultSelectionType() {
|
||||
switch roomJoinRule {
|
||||
case .restricted:
|
||||
@@ -212,82 +188,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
default:
|
||||
selectedType = .private
|
||||
}
|
||||
}
|
||||
|
||||
private func upgradeRoom(to restrictedVersionOverride: String, inviteUsers userIds: [String], completion: @escaping (Bool, String) -> Void) {
|
||||
// Need to disable graph update during this process as a lot of syncs will occure
|
||||
session.spaceService.graphUpdateEnabled = false
|
||||
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.session.spaceService.graphUpdateEnabled = true
|
||||
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.session.spaceService.graphUpdateEnabled = true
|
||||
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)
|
||||
if selectedType != .restricted {
|
||||
roomUpgradeRequiredSubject.send(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -32,10 +32,11 @@ class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
private(set) var errorSubject: CurrentValueSubject<Error?, Never>
|
||||
|
||||
private(set) var selectedType: RoomAccessTypeChooserAccessType = .private
|
||||
var currentRoomId: String {
|
||||
return "sldkfjsdljf:,atrix.org"
|
||||
var currentRoomId: String = "!aaabaa:matrix.org"
|
||||
var versionOverride: String? {
|
||||
return "9"
|
||||
}
|
||||
|
||||
|
||||
init(accessItems: [RoomAccessTypeChooserAccessItem] = mockAccessItems) {
|
||||
accessItemsSubject = CurrentValueSubject(accessItems)
|
||||
roomUpgradeRequiredSubject = CurrentValueSubject(false)
|
||||
@@ -51,8 +52,8 @@ class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
||||
|
||||
}
|
||||
|
||||
func upgradeRoom(accepted: Bool, autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) {
|
||||
|
||||
func updateRoomId(with roomId: String) {
|
||||
currentRoomId = roomId
|
||||
}
|
||||
|
||||
func applySelection(completion: @escaping () -> Void) {
|
||||
|
||||
+2
-1
@@ -26,8 +26,9 @@ protocol RoomAccessTypeChooserServiceProtocol {
|
||||
|
||||
var selectedType: RoomAccessTypeChooserAccessType { get }
|
||||
var currentRoomId: String { get }
|
||||
var versionOverride: String? { get }
|
||||
|
||||
func updateSelection(with selectedType: RoomAccessTypeChooserAccessType)
|
||||
func applySelection(completion: @escaping () -> Void)
|
||||
func upgradeRoom(accepted: Bool, autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void)
|
||||
func updateRoomId(with roomId: String)
|
||||
}
|
||||
|
||||
+2
-10
@@ -32,16 +32,7 @@ struct RoomAccessTypeChooser: View {
|
||||
|
||||
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)
|
||||
}
|
||||
.waitOverlay(show: viewModel.isLoading, message: viewModel.waitingMessage, allowUserInteraction: false)
|
||||
.navigationTitle(VectorL10n.roomAccessSettingsScreenNavTitle)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
@@ -57,6 +48,7 @@ struct RoomAccessTypeChooser: View {
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
+3
-9
@@ -44,15 +44,9 @@ struct RoomAccessTypeChooserRow: View {
|
||||
.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)
|
||||
}
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(isSelected ? theme.colors.accent : theme.colors.quarterlyContent)
|
||||
}
|
||||
if let badgeText = badgeText {
|
||||
Text(badgeText)
|
||||
|
||||
-4
@@ -40,10 +40,6 @@ class RoomRestrictedAccessSpaceChooserItemsProcessor: MatrixItemChooserProcessor
|
||||
}
|
||||
|
||||
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):
|
||||
|
||||
+4
-1
@@ -33,18 +33,21 @@ struct RoomRestrictedAccessSpaceChooserSelector: View {
|
||||
MatrixItemChooser(viewModel: viewModel, listBottomPadding: nil)
|
||||
.background(theme.colors.background)
|
||||
.navigationTitle(VectorL10n.roomAccessSettingsScreenNavTitle)
|
||||
// .navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
}
|
||||
.foregroundColor(viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent)
|
||||
.opacity(viewModel.viewState.loading ? 0.7 : 1)
|
||||
.disabled(viewModel.viewState.loading)
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(VectorL10n.done) {
|
||||
viewModel.send(viewAction: .done)
|
||||
}
|
||||
.foregroundColor(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent)
|
||||
.opacity(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? 0.7 : 1)
|
||||
.disabled(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-17
@@ -23,6 +23,7 @@ enum RoomSuggestionCoordinatorCoordinatorAction {
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@available(iOS 14.0, *)
|
||||
final class RoomSuggestionCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -52,21 +53,19 @@ final class RoomSuggestionCoordinator: Coordinator {
|
||||
|
||||
|
||||
func start() {
|
||||
if #available(iOS 14.0, *) {
|
||||
MXLog.debug("[RoomAccessCoordinator] did start.")
|
||||
let rootCoordinator = self.createRoomSuggestionSpaceChooser()
|
||||
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)
|
||||
}
|
||||
MXLog.debug("[RoomSuggestionCoordinator] did start.")
|
||||
let rootCoordinator = self.createRoomSuggestionSpaceChooser()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +76,6 @@ final class RoomSuggestionCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func pushScreen(with coordinator: Coordinator & Presentable) {
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
@@ -88,7 +86,6 @@ final class RoomSuggestionCoordinator: Coordinator {
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private func createRoomSuggestionSpaceChooser() -> MatrixItemChooserCoordinator {
|
||||
let paramaters = MatrixItemChooserCoordinatorParameters(
|
||||
session: parameters.room.mxSession,
|
||||
|
||||
+3
@@ -15,6 +15,7 @@
|
||||
//
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
@objc protocol RoomSuggestionCoordinatorBridgePresenterDelegate {
|
||||
func roomSuggestionCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: RoomSuggestionCoordinatorBridgePresenter)
|
||||
func roomSuggestionCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomSuggestionCoordinatorBridgePresenter)
|
||||
@@ -25,6 +26,7 @@ import UIKit
|
||||
/// 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
|
||||
@available(iOS 14.0, *)
|
||||
final class RoomSuggestionCoordinatorBridgePresenter: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -85,6 +87,7 @@ final class RoomSuggestionCoordinatorBridgePresenter: NSObject {
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension RoomSuggestionCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
|
||||
|
||||
+2
@@ -99,6 +99,8 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// (Un)suggest room for spaces which ID is in `parentIds`.
|
||||
/// Recurse to the next index once done.
|
||||
private func setRoom(suggested: Bool, forParentsWithId parentIds: [String], at index: Int = 0, completion: @escaping () -> Void) {
|
||||
guard index < parentIds.count else {
|
||||
completion()
|
||||
|
||||
@@ -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
|
||||
import MatrixSDK
|
||||
|
||||
struct RoomUpgradeCoordinatorParameters {
|
||||
let session: MXSession
|
||||
let roomId: String
|
||||
let versionOverride: String
|
||||
}
|
||||
|
||||
final class RoomUpgradeCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: RoomUpgradeCoordinatorParameters
|
||||
private let roomUpgradeHostingController: UIViewController
|
||||
private var roomUpgradeViewModel: RoomUpgradeViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: ((RoomUpgradeCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: RoomUpgradeCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeService(session: parameters.session, roomId: parameters.roomId, versionOverride: parameters.versionOverride))
|
||||
let view = RoomUpgrade(viewModel: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||
roomUpgradeViewModel = viewModel
|
||||
roomUpgradeHostingController = VectorHostingController(rootView: view)
|
||||
roomUpgradeHostingController.view.backgroundColor = .clear
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
MXLog.debug("[RoomUpgradeCoordinator] did start.")
|
||||
roomUpgradeViewModel.completion = { [weak self] result in
|
||||
MXLog.debug("[RoomUpgradeCoordinator] RoomUpgradeViewModel did complete with result: \(result).")
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .cancel(let roomId):
|
||||
self.completion?(.cancel(roomId))
|
||||
case .done(let roomId):
|
||||
self.completion?(.done(roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.roomUpgradeHostingController
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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 MockRoomUpgradeScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case initial
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
RoomUpgrade.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockRoomUpgradeScreenState] {
|
||||
[.initial]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let service: MockRoomUpgradeService
|
||||
switch self {
|
||||
case .initial:
|
||||
service = MockRoomUpgradeService()
|
||||
}
|
||||
let viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[service, viewModel],
|
||||
AnyView(RoomUpgrade(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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 RoomUpgradeCoordinatorResult {
|
||||
case cancel(_ roomId: String)
|
||||
case done(_ roomId: String)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum RoomUpgradeViewModelResult {
|
||||
case cancel(_ roomId: String)
|
||||
case done(_ roomId: String)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct RoomUpgradeViewState: BindableState {
|
||||
var bindings: RoomUpgradeViewModelBindings
|
||||
}
|
||||
|
||||
struct RoomUpgradeViewModelBindings {
|
||||
var waitingMessage: String?
|
||||
var isLoading: Bool
|
||||
}
|
||||
|
||||
enum RoomUpgradeViewAction {
|
||||
case cancel
|
||||
case done(_ autoInviteUsers: Bool)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// 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 RoomUpgradeViewModelType = StateStoreViewModel<RoomUpgradeViewState,
|
||||
Never,
|
||||
RoomUpgradeViewAction>
|
||||
@available(iOS 14, *)
|
||||
class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let roomUpgradeService: RoomUpgradeServiceProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((RoomUpgradeViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
static func makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewModelProtocol {
|
||||
return RoomUpgradeViewModel(roomUpgradeService: roomUpgradeService)
|
||||
}
|
||||
|
||||
private init(roomUpgradeService: RoomUpgradeServiceProtocol) {
|
||||
self.roomUpgradeService = roomUpgradeService
|
||||
super.init(initialViewState: Self.defaultState(roomUpgradeService: roomUpgradeService))
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private static func defaultState(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewState {
|
||||
let bindings = RoomUpgradeViewModelBindings(waitingMessage: nil, isLoading: false)
|
||||
return RoomUpgradeViewState(bindings: bindings)
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
roomUpgradeService
|
||||
.upgradingSubject
|
||||
.sink { [weak self] isUpgrading in
|
||||
self?.state.bindings = RoomUpgradeViewModelBindings(waitingMessage: isUpgrading ? VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgrading: nil, isLoading: isUpgrading)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: RoomUpgradeViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
completion?(.cancel(roomUpgradeService.currentRoomId))
|
||||
case .done(let autoInviteUsers):
|
||||
roomUpgradeService.upgradeRoom(autoInviteUsers: autoInviteUsers) { [weak self] success, roomId in
|
||||
guard let self = self else { return }
|
||||
if success {
|
||||
self.completion?(.done(self.roomUpgradeService.currentRoomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// 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 RoomUpgradeViewModelProtocol {
|
||||
|
||||
var completion: ((RoomUpgradeViewModelResult) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
static func makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewModelProtocol
|
||||
@available(iOS 14, *)
|
||||
var context: RoomUpgradeViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// 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
|
||||
import MatrixSDK
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class RoomUpgradeService: RoomUpgradeServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let session: MXSession
|
||||
|
||||
private let versionOverride: String
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
private var didBuildSpaceGraphObserver: Any?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
private(set) var upgradingSubject: CurrentValueSubject<Bool, Never>
|
||||
private(set) var errorSubject: CurrentValueSubject<Error?, Never>
|
||||
private(set) var currentRoomId: String
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession, roomId: String, versionOverride: String) {
|
||||
self.session = session
|
||||
self.currentRoomId = roomId
|
||||
self.versionOverride = versionOverride
|
||||
self.upgradingSubject = CurrentValueSubject(false)
|
||||
self.errorSubject = CurrentValueSubject(nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
currentOperation?.cancel()
|
||||
if let observer = self.didBuildSpaceGraphObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) {
|
||||
upgradingSubject.send(true)
|
||||
|
||||
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: self.versionOverride, inviteUsers: memberIds, completion: completion)
|
||||
case .failure(let error):
|
||||
self.upgradingSubject.send(false)
|
||||
self.errorSubject.send(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.upgradeRoom(to: versionOverride, inviteUsers: [], completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func upgradeRoom(to versionOverride: String, inviteUsers userIds: [String], completion: @escaping (Bool, String) -> Void) {
|
||||
// Need to disable graph update during this process as a lot of syncs will occure
|
||||
session.spaceService.graphUpdateEnabled = false
|
||||
currentOperation = session.matrixRestClient.upgradeRoom(withId: self.currentRoomId, to: versionOverride) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch response {
|
||||
case .success(let replacementRoomId):
|
||||
let oldRoomId = self.currentRoomId
|
||||
self.currentRoomId = replacementRoomId
|
||||
let parentSpaces = self.session.spaceService.directParentIds(ofRoomWithId: oldRoomId)
|
||||
self.moveRoom(from: oldRoomId, to: replacementRoomId, within: Array(parentSpaces), at: 0) {
|
||||
self.session.spaceService.graphUpdateEnabled = true
|
||||
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.session.spaceService.graphUpdateEnabled = true
|
||||
self.upgradingSubject.send(false)
|
||||
self.errorSubject.send(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move room with roomId to new room ID for each space which ID belongs to`parentIds` list.
|
||||
/// Recurse to the next index once done.
|
||||
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("[RoomUpgradeService] 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("[RoomUpgradeService] 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Invite all users within `userIds` list
|
||||
/// Recurse to the next index once done.
|
||||
private func inviteUser(from userIds: [String], at index: Int, completion: @escaping (Bool, String) -> Void) {
|
||||
guard index < userIds.count else {
|
||||
self.upgradingSubject.send(false)
|
||||
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("[RoomUpgradeService] 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,35 @@
|
||||
//
|
||||
// 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 MockRoomUpgradeService: RoomUpgradeServiceProtocol {
|
||||
var currentRoomId: String = "!sfdlksjdflkfjds:matrix.org"
|
||||
|
||||
var errorSubject: CurrentValueSubject<Error?, Never>
|
||||
var upgradingSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
init() {
|
||||
self.errorSubject = CurrentValueSubject(nil)
|
||||
self.upgradingSubject = CurrentValueSubject(false)
|
||||
}
|
||||
|
||||
func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// 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 RoomUpgradeServiceProtocol {
|
||||
var currentRoomId: String { get }
|
||||
var upgradingSubject: CurrentValueSubject<Bool, Never> { get }
|
||||
var errorSubject: CurrentValueSubject<Error?, Never> { get }
|
||||
|
||||
func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void)
|
||||
}
|
||||
@@ -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 RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class RoomUpgradeUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockRoomUpgradeScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return RoomUpgradeUITests(selector: #selector(verifyRoomUpgradeScreen))
|
||||
}
|
||||
|
||||
func verifyRoomUpgradeScreen() throws {
|
||||
guard let screenState = screenState as? MockRoomUpgradeScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .presence(let presence):
|
||||
verifyRoomUpgradePresence(presence: presence)
|
||||
case .longDisplayName(let name):
|
||||
verifyRoomUpgradeLongName(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyRoomUpgradePresence(presence: RoomUpgradePresence) {
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssertEqual(presenceText.label, presence.title)
|
||||
}
|
||||
|
||||
func verifyRoomUpgradeLongName(name: String) {
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssertEqual(displayNameText.label, name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// 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 RoomUpgradeViewModelTests: XCTestCase {
|
||||
private enum Constants {
|
||||
static let presenceInitialValue: RoomUpgradePresence = .offline
|
||||
static let displayName = "Alice"
|
||||
}
|
||||
var service: MockRoomUpgradeService!
|
||||
var viewModel: RoomUpgradeViewModelProtocol!
|
||||
var context: RoomUpgradeViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
override func setUpWithError() throws {
|
||||
service = MockRoomUpgradeService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
|
||||
viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: service)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(context.viewState.displayName, Constants.displayName)
|
||||
XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue)
|
||||
}
|
||||
|
||||
func testFirstPresenceReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first()
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
|
||||
}
|
||||
|
||||
func testPresenceUpdatesReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(3).first()
|
||||
let awaitDeferred = xcAwaitDeferred(presencePublisher)
|
||||
let newPresenceValue1: RoomUpgradePresence = .online
|
||||
let newPresenceValue2: RoomUpgradePresence = .idle
|
||||
service.simulateUpdate(presence: newPresenceValue1)
|
||||
service.simulateUpdate(presence: newPresenceValue2)
|
||||
XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
|
||||
}
|
||||
}
|
||||
+22
-13
@@ -17,27 +17,27 @@
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct RoomAccessTypeChooserUpgradeRoomAlert: View {
|
||||
|
||||
struct RoomUpgrade: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@ObservedObject var viewModel: RoomAccessTypeChooserViewModelType.Context
|
||||
@State var autoInviteUsers: Bool = true
|
||||
|
||||
// MARK: - Private
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: RoomUpgradeViewModel.Context
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(0.6)
|
||||
alertContent
|
||||
.modifier(WaitOverlay(
|
||||
allowUserInteraction: false,
|
||||
message: $viewModel.waitingMessage,
|
||||
isLoading: $viewModel.isLoading))
|
||||
.waitOverlay(show: viewModel.isLoading, message: viewModel.waitingMessage, allowUserInteraction: false)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
@@ -58,17 +58,17 @@ struct RoomAccessTypeChooserUpgradeRoomAlert: View {
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.padding(.bottom, 35)
|
||||
.padding(.horizontal, 12)
|
||||
Toggle(isOn: $autoInviteUsers, label: {
|
||||
Toggle(isOn: $autoInviteUsers) {
|
||||
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))
|
||||
viewModel.send(viewAction: .done(autoInviteUsers))
|
||||
} label: {
|
||||
Text(VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgradeButton)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ struct RoomAccessTypeChooserUpgradeRoomAlert: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 16)
|
||||
Button {
|
||||
viewModel.send(viewAction: .didCancelRoomUpgrade)
|
||||
viewModel.send(viewAction: .cancel)
|
||||
} label: {
|
||||
Text(VectorL10n.cancel)
|
||||
}
|
||||
@@ -92,3 +92,12 @@ struct RoomAccessTypeChooserUpgradeRoomAlert: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct RoomUpgrade_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockRoomUpgradeScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user