diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index be085a6ef5..46e0833a3a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -69,7 +69,11 @@ "private" = "Private"; "public" = "Public"; "stop" = "Stop"; +"new_word" = "New"; +"existing" = "Existing"; +"add" = "Add"; "ok" = "OK"; +"error" = "Error"; // Call Bar "callbar_only_single_active" = "Tap to return to the call (%@)"; @@ -1692,20 +1696,27 @@ Tap the + to start adding people."; // MARK: - Create Room "create_room_title" = "New Room"; -"create_room_section_header_name" = "Room name"; +"create_room_section_header_name" = "NAME"; "create_room_placeholder_name" = "Name"; -"create_room_section_header_topic" = "Room topic (optional)"; -"create_room_placeholder_topic" = "Topic"; -"create_room_section_header_encryption" = "Room encryption"; +"create_room_section_header_topic" = "TOPIC (OPTIONAL)"; +"create_room_placeholder_topic" = "What is this room about?"; +"create_room_section_header_encryption" = "ENCRYPTION"; "create_room_enable_encryption" = "Enable Encryption"; "create_room_section_footer_encryption" = "Encryption can’t be disabled afterwards."; -"create_room_section_header_type" = "Room type"; -"create_room_type_private" = "Private Room"; -"create_room_type_public" = "Public Room"; -"create_room_section_footer_type" = "People join a private room only with the room invitation."; -"create_room_show_in_directory" = "Show the room in the directory"; -"create_room_section_header_address" = "Room address"; +"create_room_section_header_type" = "WHO CAN ACCESS"; +"create_room_type_private" = "Private Room (invite only)"; +"create_room_type_restricted" = "Space members"; +"create_room_type_public" = "Public Room (anyone)"; +"create_room_section_footer_type_private" = "Only people invited can find and join."; +"create_room_section_footer_type_restricted" = "Anyone in Space name can find and join."; +"create_room_section_footer_type_public" = "Only people invited can find and join, not just people in Space name."; +"create_room_promotion_header" = "PROMOTION"; +"create_room_show_in_directory" = "Show in room directory"; +"create_room_show_in_directory_footer" = "This will help people find and join."; +"create_room_section_header_address" = "ADDRESS"; "create_room_placeholder_address" = "#testroom:matrix.org"; +"create_room_suggest_room" = "Suggest to space members"; +"create_room_suggest_room_footer" = "Suggested rooms are promoted to space members as good to join."; // MARK: - Room Info @@ -1888,6 +1899,11 @@ Tap the + to start adding people."; "spaces_creation_in_many_spaces" = "in %@ spaces"; "spaces_creation_in_one_space" = "in 1 space"; +"spaces_invite_people" = "Invite people"; +"spaces_add_room" = "Add room"; +"spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; +"spaces_add_space" = "Add space"; + // Mark: Avatar "space_avatar_view_accessibility_label" = "avatar"; diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift index e1c16760ef..6846d8a646 100644 --- a/Riot/Categories/UIViewController.swift +++ b/Riot/Categories/UIViewController.swift @@ -44,28 +44,50 @@ extension UIViewController { /// - Parameters: /// - viewController: The child view controller to add. /// - view: The view on which to add the child view controller view. - func vc_addChildViewController(viewController: UIViewController, onView view: UIView) { + /// - animated: true to add a fade in animation + func vc_addChildViewController(viewController: UIViewController, onView view: UIView, animated: Bool = false) { self.addChild(viewController) viewController.view.frame = view.bounds + if animated { + viewController.view.alpha = 0 + } view.vc_addSubViewMatchingParent(viewController.view) + if animated { + UIView.animate(withDuration: 0.2) { + viewController.view.alpha = 1 + } + } viewController.didMove(toParent: self) } /// Remove a child view controller from current view controller. /// - /// - Parameter viewController: The child view controller to remove. - func vc_removeChildViewController(viewController: UIViewController) { + /// - Parameters: + /// - viewController: The child view controller to remove. + /// - animated: true to add a fade out animation + func vc_removeChildViewController(viewController: UIViewController, animated: Bool = false) { viewController.willMove(toParent: nil) - viewController.view.removeFromSuperview() + if animated { + UIView.animate(withDuration: 0.2) { + viewController.view.alpha = 0 + } completion: { finished in + viewController.view.removeFromSuperview() + viewController.view.alpha = 1 + } + } else { + viewController.view.removeFromSuperview() + } viewController.removeFromParent() } /// Remove current view controller from parent. - func vc_removeFromParent() { - self.vc_removeChildViewController(viewController: self) + /// + /// - Parameter animated: true to add a fade out animation + func vc_removeFromParent(animated: Bool = false) { + self.vc_removeChildViewController(viewController: self, animated: animated) } /// Adds a floating action button to the bottom-right of the page. diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 31d79cac7a..b133d3919e 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -35,6 +35,10 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } + /// Add + public static var add: String { + return VectorL10n.tr("Vector", "add") + } /// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. public static func analyticsPromptMessageNewUser(_ p1: String) -> String { return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1) @@ -659,54 +663,82 @@ public class VectorL10n: NSObject { public static var createRoomPlaceholderName: String { return VectorL10n.tr("Vector", "create_room_placeholder_name") } - /// Topic + /// What is this room about? public static var createRoomPlaceholderTopic: String { return VectorL10n.tr("Vector", "create_room_placeholder_topic") } + /// PROMOTION + public static var createRoomPromotionHeader: String { + return VectorL10n.tr("Vector", "create_room_promotion_header") + } /// Encryption can’t be disabled afterwards. public static var createRoomSectionFooterEncryption: String { return VectorL10n.tr("Vector", "create_room_section_footer_encryption") } - /// People join a private room only with the room invitation. - public static var createRoomSectionFooterType: String { - return VectorL10n.tr("Vector", "create_room_section_footer_type") + /// Only people invited can find and join. + public static var createRoomSectionFooterTypePrivate: String { + return VectorL10n.tr("Vector", "create_room_section_footer_type_private") } - /// Room address + /// Only people invited can find and join, not just people in Space name. + public static var createRoomSectionFooterTypePublic: String { + return VectorL10n.tr("Vector", "create_room_section_footer_type_public") + } + /// Anyone in Space name can find and join. + public static var createRoomSectionFooterTypeRestricted: String { + return VectorL10n.tr("Vector", "create_room_section_footer_type_restricted") + } + /// ADDRESS public static var createRoomSectionHeaderAddress: String { return VectorL10n.tr("Vector", "create_room_section_header_address") } - /// Room encryption + /// ENCRYPTION public static var createRoomSectionHeaderEncryption: String { return VectorL10n.tr("Vector", "create_room_section_header_encryption") } - /// Room name + /// NAME public static var createRoomSectionHeaderName: String { return VectorL10n.tr("Vector", "create_room_section_header_name") } - /// Room topic (optional) + /// TOPIC (OPTIONAL) public static var createRoomSectionHeaderTopic: String { return VectorL10n.tr("Vector", "create_room_section_header_topic") } - /// Room type + /// WHO CAN ACCESS public static var createRoomSectionHeaderType: String { return VectorL10n.tr("Vector", "create_room_section_header_type") } - /// Show the room in the directory + /// Show in room directory public static var createRoomShowInDirectory: String { return VectorL10n.tr("Vector", "create_room_show_in_directory") } + /// This will help people find and join. + public static var createRoomShowInDirectoryFooter: String { + return VectorL10n.tr("Vector", "create_room_show_in_directory_footer") + } + /// Suggest to space members + public static var createRoomSuggestRoom: String { + return VectorL10n.tr("Vector", "create_room_suggest_room") + } + /// Suggested rooms are promoted to space members as good to join. + public static var createRoomSuggestRoomFooter: String { + return VectorL10n.tr("Vector", "create_room_suggest_room_footer") + } /// New Room public static var createRoomTitle: String { return VectorL10n.tr("Vector", "create_room_title") } - /// Private Room + /// Private Room (invite only) public static var createRoomTypePrivate: String { return VectorL10n.tr("Vector", "create_room_type_private") } - /// Public Room + /// Public Room (anyone) public static var createRoomTypePublic: String { return VectorL10n.tr("Vector", "create_room_type_public") } + /// Space members + public static var createRoomTypeRestricted: String { + return VectorL10n.tr("Vector", "create_room_type_restricted") + } /// Verify your other devices easier public static var crossSigningSetupBannerSubtitle: String { return VectorL10n.tr("Vector", "cross_signing_setup_banner_subtitle") @@ -1303,6 +1335,10 @@ public class VectorL10n: NSObject { public static var encryptedRoomMessageReplyToPlaceholder: String { return VectorL10n.tr("Vector", "encrypted_room_message_reply_to_placeholder") } + /// Error + public static var error: String { + return VectorL10n.tr("Vector", "error") + } /// Add an identity server in your settings to invite by email. public static var errorInvite3pidWithNoIdentityServer: String { return VectorL10n.tr("Vector", "error_invite_3pid_with_no_identity_server") @@ -1451,6 +1487,10 @@ public class VectorL10n: NSObject { public static func eventFormatterWidgetRemovedByYou(_ p1: String) -> String { return VectorL10n.tr("Vector", "event_formatter_widget_removed_by_you", p1) } + /// Existing + public static var existing: String { + return VectorL10n.tr("Vector", "existing") + } /// The link %@ is taking you to another site: %@\n\nAre you sure you want to continue? public static func externalLinkConfirmationMessage(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "external_link_confirmation_message", p1, p2) @@ -2379,6 +2419,10 @@ public class VectorL10n: NSObject { public static var networkOfflinePrompt: String { return VectorL10n.tr("Vector", "network_offline_prompt") } + /// New + public static var newWord: String { + return VectorL10n.tr("Vector", "new_word") + } /// Next public static var next: String { return VectorL10n.tr("Vector", "next") @@ -5375,6 +5419,10 @@ public class VectorL10n: NSObject { public static var spacesAddRoom: String { return VectorL10n.tr("Vector", "spaces_add_room") } + /// You do not have permissions to add rooms to this space. + public static var spacesAddRoomMissingPermissionMessage: String { + return VectorL10n.tr("Vector", "spaces_add_room_missing_permission_message") + } /// Adding rooms coming soon public static var spacesAddRoomsComingSoonTitle: String { return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title") diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 0a7b5a151d..6b4d6fc629 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1989,7 +1989,8 @@ - (void)createNewRoom // Sanity check if (self.mainSession) { - self.createRoomCoordinatorBridgePresenter = [[CreateRoomCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + CreateRoomCoordinatorParameter *parameters = [[CreateRoomCoordinatorParameter alloc] initWithSession:self.mainSession parentSpace: self.dataSource.currentSpace]; + self.createRoomCoordinatorBridgePresenter = [[CreateRoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; self.createRoomCoordinatorBridgePresenter.delegate = self; [self.createRoomCoordinatorBridgePresenter presentFrom:self animated:YES]; } @@ -2197,6 +2198,12 @@ - (void)createRoomCoordinatorBridgePresenterDelegateDidCancel:(CreateRoomCoordin coordinatorBridgePresenter = nil; } +- (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didAddRoomsWithIds:(NSArray *)roomIds +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + coordinatorBridgePresenter = nil; +} + #pragma mark - Empty view management - (void)showEmptyViewIfNeeded diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift index e268d95285..8bc2f96d5d 100644 --- a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift +++ b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift @@ -18,6 +18,21 @@ import UIKit +@objcMembers +class CreateRoomCoordinatorParameter: NSObject { + /// Instance of the current MXSession + let session: MXSession + + /// Instance of the parent space. `nil` if home space + let parentSpace: MXSpace? + + init(session: MXSession, + parentSpace: MXSpace?) { + self.session = session + self.parentSpace = parentSpace + } +} + @objcMembers final class CreateRoomCoordinator: CreateRoomCoordinatorType { @@ -26,7 +41,8 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType { // MARK: Private private let navigationRouter: NavigationRouterType - private let session: MXSession + private let tabRouter: TabbedRouterType + private let parameters: CreateRoomCoordinatorParameter // MARK: Public @@ -35,25 +51,55 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType { weak var delegate: CreateRoomCoordinatorDelegate? + var parentSpace: MXSpace? { + parameters.parentSpace + } + // MARK: - Setup - init(session: MXSession) { + init(parameters: CreateRoomCoordinatorParameter) { self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController()) - self.session = session + let segmentedController = SegmentedController.instantiate() + segmentedController.title = VectorL10n.spacesAddRoom + self.tabRouter = SegmentedRouter(segmentedController: segmentedController) + self.parameters = parameters } // MARK: - Public methods func start() { - let rootCoordinator = self.createEnterNewRoomDetailsCoordinator() + let createRoomCoordinator = self.createEnterNewRoomDetailsCoordinator() - rootCoordinator.start() + createRoomCoordinator.start() - self.add(childCoordinator: rootCoordinator) + self.add(childCoordinator: createRoomCoordinator) - self.navigationRouter.setRootModule(rootCoordinator) - } + if let parentSpace = self.parentSpace, #available(iOS 14, *) { + let roomSelectionCoordinator = self.createRoomSelectorCoordinator(parentSpace: parentSpace) + roomSelectionCoordinator.completion = { [weak self] result in + guard let self = self else { + return + } + + switch result { + case .done(let selectedItemIds): + self.delegate?.createRoomCoordinator(self, didAddRoomsWithIds: selectedItemIds) + default: + self.delegate?.createRoomCoordinatorDidCancel(self) + } + } + roomSelectionCoordinator.start() + self.add(childCoordinator: roomSelectionCoordinator) + self.tabRouter.tabs = [ + TabbedRouterTab(title: VectorL10n.newWord, icon: nil, module: createRoomCoordinator), + TabbedRouterTab(title: VectorL10n.existing, icon: nil, module: roomSelectionCoordinator) + ] + self.navigationRouter.setRootModule(self.tabRouter) + } else { + self.navigationRouter.setRootModule(createRoomCoordinator) + } + } func toPresentable() -> UIViewController { return self.navigationRouter.toPresentable() @@ -62,10 +108,17 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType { // MARK: - Private methods private func createEnterNewRoomDetailsCoordinator() -> EnterNewRoomDetailsCoordinator { - let coordinator = EnterNewRoomDetailsCoordinator(session: self.session) + let coordinator = EnterNewRoomDetailsCoordinator(session: self.parameters.session, parentSpace: self.parentSpace) coordinator.delegate = self return coordinator } + + @available(iOS 14.0, *) + private func createRoomSelectorCoordinator(parentSpace: MXSpace) -> MatrixItemChooserCoordinator { + let paramaters = MatrixItemChooserCoordinatorParameters(session: self.parameters.session, viewProvider: AddRoomSelectorViewProvider(), itemsProcessor: AddRoomItemsProcessor(parentSpace: parentSpace)) + let coordinator = MatrixItemChooserCoordinator(parameters: paramaters) + return coordinator + } } // MARK: - EnterNewRoomDetailsCoordinatorDelegate diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift index 44bc7d2b87..24efedb4c0 100644 --- a/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift @@ -20,6 +20,7 @@ import Foundation @objc protocol CreateRoomCoordinatorBridgePresenterDelegate { func createRoomCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter, didCreateNewRoom room: MXRoom) + func createRoomCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter, didAddRoomsWithIds roomIds: [String]) func createRoomCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter) } @@ -32,7 +33,7 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject { // MARK: Private - private let session: MXSession + private let parameters: CreateRoomCoordinatorParameter private var coordinator: CreateRoomCoordinator? // MARK: Public @@ -41,8 +42,8 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject { // MARK: - Setup - init(session: MXSession) { - self.session = session + init(parameters: CreateRoomCoordinatorParameter) { + self.parameters = parameters super.init() } @@ -54,7 +55,7 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject { // } func present(from viewController: UIViewController, animated: Bool) { - let createRoomCoordinator = CreateRoomCoordinator(session: self.session) + let createRoomCoordinator = CreateRoomCoordinator(parameters: self.parameters) createRoomCoordinator.delegate = self let presentable = createRoomCoordinator.toPresentable() presentable.presentationController?.delegate = self @@ -86,6 +87,10 @@ extension CreateRoomCoordinatorBridgePresenter: CreateRoomCoordinatorDelegate { self.delegate?.createRoomCoordinatorBridgePresenterDelegate(self, didCreateNewRoom: room) } + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) { + self.delegate?.createRoomCoordinatorBridgePresenterDelegate(self, didAddRoomsWithIds: roomIds) + } + func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) { self.delegate?.createRoomCoordinatorBridgePresenterDelegateDidCancel(self) } diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift index 585538ddba..8c1b36c359 100644 --- a/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift +++ b/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift @@ -20,10 +20,12 @@ import Foundation protocol CreateRoomCoordinatorDelegate: AnyObject { func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) } /// `CreateRoomCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. protocol CreateRoomCoordinatorType: Coordinator, Presentable { var delegate: CreateRoomCoordinatorDelegate? { get } + var parentSpace: MXSpace? { get } } diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift index f310f442c8..9a03641f73 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift @@ -26,6 +26,7 @@ final class EnterNewRoomDetailsCoordinator: EnterNewRoomDetailsCoordinatorType { // MARK: Private private let session: MXSession + private let parentSpace: MXSpace? private var enterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType private let enterNewRoomDetailsViewController: EnterNewRoomDetailsViewController @@ -44,10 +45,11 @@ final class EnterNewRoomDetailsCoordinator: EnterNewRoomDetailsCoordinatorType { // MARK: - Setup - init(session: MXSession) { + init(session: MXSession, parentSpace: MXSpace?) { self.session = session + self.parentSpace = parentSpace - let enterNewRoomDetailsViewModel = EnterNewRoomDetailsViewModel(session: self.session) + let enterNewRoomDetailsViewModel = EnterNewRoomDetailsViewModel(session: self.session, parentSpace: self.parentSpace) let enterNewRoomDetailsViewController = EnterNewRoomDetailsViewController.instantiate(with: enterNewRoomDetailsViewModel) self.enterNewRoomDetailsViewModel = enterNewRoomDetailsViewModel self.enterNewRoomDetailsViewController = enterNewRoomDetailsViewController diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift index 63fe651974..aa42f667c6 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift @@ -47,7 +47,14 @@ final class EnterNewRoomDetailsViewController: UIViewController { private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! private lazy var createBarButtonItem: MXKBarButtonItem = { - let item = MXKBarButtonItem(title: VectorL10n.create, style: .plain) { [weak self] in + let title: String + switch viewModel.actionType { + case .createAndAddToSpace: + title = VectorL10n.add + case .createOnly: + title = VectorL10n.create + } + let item = MXKBarButtonItem(title: title, style: .plain) { [weak self] in self?.createButtonAction() }! item.isEnabled = false @@ -118,30 +125,59 @@ final class EnterNewRoomDetailsViewController: UIViewController { var section4: Section? if RiotSettings.shared.roomCreationScreenAllowRoomTypeConfiguration { - let row_4_0 = Row(type: .default, text: VectorL10n.createRoomTypePrivate, accessoryType: viewModel.roomCreationParameters.isPublic ? .none : .checkmark) { [weak self] in + let row_4_0 = Row(type: .default, text: VectorL10n.createRoomTypePrivate, accessoryType: viewModel.roomCreationParameters.joinRule == .private ? .checkmark : .none) { [weak self] in guard let self = self else { return } - self.viewModel.roomCreationParameters.isPublic = false + self.viewModel.roomCreationParameters.joinRule = .private self.updateSections() } - let row_4_1 = Row(type: .default, text: VectorL10n.createRoomTypePublic, accessoryType: viewModel.roomCreationParameters.isPublic ? .checkmark : .none) { [weak self] in + let row_4_1 = Row(type: .default, text: VectorL10n.createRoomTypeRestricted, accessoryType: viewModel.roomCreationParameters.joinRule == .restricted ? .checkmark : .none) { [weak self] in guard let self = self else { return } - self.viewModel.roomCreationParameters.isPublic = true + self.viewModel.roomCreationParameters.joinRule = .restricted + self.updateSections() + // scroll bottom to show user new fields + DispatchQueue.main.async { + self.mainTableView.scrollToRow(at: IndexPath(row: 0, section: 5), at: .bottom, animated: true) + } + } + let row_4_2 = Row(type: .default, text: VectorL10n.createRoomTypePublic, accessoryType: viewModel.roomCreationParameters.joinRule == .public ? .checkmark : .none) { [weak self] in + + guard let self = self else { + return + } + + self.viewModel.roomCreationParameters.joinRule = .public self.updateSections() // scroll bottom to show user new fields DispatchQueue.main.async { self.mainTableView.scrollToRow(at: IndexPath(row: 0, section: 6), at: .bottom, animated: true) } } + let rows: [Row] + switch viewModel.actionType { + case .createAndAddToSpace: + rows = [row_4_0, row_4_1, row_4_2] + case .createOnly: + rows = [row_4_0, row_4_2] + } + let footer: String + switch viewModel.roomCreationParameters.joinRule { + case .private: + footer = VectorL10n.createRoomSectionFooterTypePrivate + case .restricted: + footer = VectorL10n.createRoomSectionFooterTypeRestricted + default: + footer = VectorL10n.createRoomSectionFooterTypePublic + } section4 = Section(header: VectorL10n.createRoomSectionHeaderType, - rows: [row_4_0, row_4_1], - footer: VectorL10n.createRoomSectionFooterType) + rows: rows, + footer: footer) } var tmpSections: [Section] = [ @@ -158,15 +194,28 @@ final class EnterNewRoomDetailsViewController: UIViewController { tmpSections.append(section4) } - if viewModel.roomCreationParameters.isPublic { + if viewModel.roomCreationParameters.joinRule == .public { let row_5_0 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.showInDirectory, onValueChanged: { [weak self] (theSwitch) in self?.viewModel.roomCreationParameters.showInDirectory = theSwitch.isOn }), text: VectorL10n.createRoomShowInDirectory, accessoryType: .none) { // no-op } - let section5 = Section(header: nil, - rows: [row_5_0], - footer: nil) + + let rows: [Row] + if viewModel.actionType == .createAndAddToSpace { + let row_5_1 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.isRoomSuggested, onValueChanged: { [weak self] (theSwitch) in + self?.viewModel.roomCreationParameters.isRoomSuggested = theSwitch.isOn + }), text: VectorL10n.createRoomSuggestRoom, accessoryType: .none) { + // no-op + } + rows = [row_5_0, row_5_1] + } else { + rows = [row_5_0] + } + + let section5 = Section(header: VectorL10n.createRoomPromotionHeader, + rows: rows, + footer: VectorL10n.createRoomShowInDirectoryFooter) let row_6_0 = Row(type: .textField(tag: Constants.roomAddressTextFieldTag, placeholder: VectorL10n.createRoomPlaceholderAddress, delegate: self), text: viewModel.roomCreationParameters.address, accessoryType: .none) { @@ -178,6 +227,18 @@ final class EnterNewRoomDetailsViewController: UIViewController { tmpSections.append(contentsOf: [section5, section6]) } + if viewModel.roomCreationParameters.joinRule == .restricted { + let row_5_0 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.isRoomSuggested, onValueChanged: { [weak self] (theSwitch) in + self?.viewModel.roomCreationParameters.isRoomSuggested = theSwitch.isOn + }), text: VectorL10n.createRoomSuggestRoom, accessoryType: .none) { + // no-op + } + let section5 = Section(header: VectorL10n.createRoomPromotionHeader, + rows: [row_5_0], + footer: VectorL10n.createRoomSuggestRoomFooter) + tmpSections.append(section5) + } + sections = tmpSections } diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift index 0e4721b6e2..f95b531dc9 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift @@ -25,6 +25,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { // MARK: Private private let session: MXSession + private let parentSpace: MXSpace? private var currentOperation: MXHTTPOperation? private var mediaUploader: MXMediaLoader? @@ -41,12 +42,17 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { } } + var actionType: EnterNewRoomActionType { + parentSpace != nil ? .createAndAddToSpace : .createOnly + } + // MARK: - Setup - init(session: MXSession) { + init(session: MXSession, parentSpace: MXSpace?) { self.session = session + self.parentSpace = parentSpace roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted - roomCreationParameters.isPublic = RiotSettings.shared.roomCreationScreenRoomIsPublic + roomCreationParameters.joinRule = RiotSettings.shared.roomCreationScreenRoomIsPublic ? .public : .private viewState = .loaded } @@ -98,33 +104,37 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { } private func createRoom() { - // compose room creation parameters in Matrix level - let parameters = MXRoomCreationParameters() - parameters.name = roomCreationParameters.name - parameters.topic = roomCreationParameters.topic - parameters.roomAlias = fixRoomAlias(alias: roomCreationParameters.address) - - if roomCreationParameters.isPublic { - parameters.preset = kMXRoomPresetPublicChat - if roomCreationParameters.showInDirectory { - parameters.visibility = kMXRoomDirectoryVisibilityPublic - } else { - parameters.visibility = kMXRoomDirectoryVisibilityPrivate - } - } else { - parameters.preset = kMXRoomPresetPrivateChat - parameters.visibility = kMXRoomDirectoryVisibilityPrivate - } - - if roomCreationParameters.isEncrypted { - parameters.initialStateEvents = [MXRoomCreationParameters.initialStateEventForEncryption(withAlgorithm: kMXCryptoMegolmAlgorithm)] + guard let roomName = roomCreationParameters.name else { + fatalError("[EnterNewRoomDetailsViewModel] createRoom: room name cannot be nil.") } - viewState = .loading - - currentOperation = session.createRoom(parameters: parameters) { (response) in + currentOperation = session.createRoom( + withName: roomName, + joinRule: roomCreationParameters.joinRule, + topic: roomCreationParameters.topic, + parentRoomId: parentSpace?.spaceId, + aliasLocalPart: fixRoomAlias(alias: roomCreationParameters.address), + isEncrypted: roomCreationParameters.isEncrypted, + completion: { response in + switch response { + case .success(let room): + if let parentSpace = self.parentSpace { + self.add(room, to: parentSpace) + } else { + self.uploadAvatarIfRequired(ofRoom: room) + self.currentOperation = nil + } + case .failure(let error): + self.viewState = .error(error) + self.currentOperation = nil + } + }) + } + + private func add(_ room: MXRoom, to space: MXSpace) { + currentOperation = space.addChild(roomId: room.roomId, suggested: roomCreationParameters.isRoomSuggested) { response in switch response { - case .success(let room): + case .success: self.uploadAvatarIfRequired(ofRoom: room) self.currentOperation = nil case .failure(let error): diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift index 5ad3623ec6..95ccbcaa5a 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift @@ -28,6 +28,11 @@ protocol EnterNewRoomDetailsViewModelCoordinatorDelegate: AnyObject { func enterNewRoomDetailsViewModelDidCancel(_ viewModel: EnterNewRoomDetailsViewModelType) } +enum EnterNewRoomActionType { + case createOnly + case createAndAddToSpace +} + /// Protocol describing the view model used by `EnterNewRoomDetailsViewController` protocol EnterNewRoomDetailsViewModelType { @@ -39,4 +44,6 @@ protocol EnterNewRoomDetailsViewModelType { var roomCreationParameters: RoomCreationParameters { get set } var viewState: EnterNewRoomDetailsViewState { get } + + var actionType: EnterNewRoomActionType { get } } diff --git a/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift b/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift index f7149fc3ad..6ea63db136 100644 --- a/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift +++ b/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift @@ -24,16 +24,22 @@ struct RoomCreationParameters { return userSelectedAvatar ?? initialsAvatar } var isEncrypted: Bool = false - var isPublic: Bool = false { + var joinRule: MXRoomJoinRule = .private { didSet { - if !isPublic { - // if set to private again, reset some fields + switch joinRule { + case .restricted: showInDirectory = false address = nil + case .private: + showInDirectory = false + address = nil + isRoomSuggested = false + default: break } } } var showInDirectory: Bool = false + var isRoomSuggested: Bool = false var userSelectedAvatar: UIImage? var initialsAvatar: UIImage? diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 95da3b651e..8e49c205a1 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -296,18 +296,6 @@ - (void)onMatrixSessionChange [self updateEmptyView]; } -- (void)createNewRoom -{ - if (recentsDataSource.currentSpace) - { - [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.spacesAddRoomsComingSoonTitle message:[VectorL10n spacesComingSoonDetail:AppInfo.current.displayName]]; - } - else - { - [super createNewRoom]; - } -} - - (void)startChat { if (recentsDataSource.currentSpace) { @@ -321,6 +309,22 @@ - (void)startChat { } } +- (void)createNewRoom +{ + if (recentsDataSource.currentSpace) { + [recentsDataSource.currentSpace canAddRoomWithCompletion:^(BOOL canAddRoom) { + if (canAddRoom) { + [super createNewRoom]; + } else { + [[AppDelegate theDelegate] showAlertWithTitle:[VectorL10n roomRecentsCreateEmptyRoom] + message:[VectorL10n spacesAddRoomMissingPermissionMessage]]; + } + }]; + } else { + [super createNewRoom]; + } +} + #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift index e4e3fedeec..ad883ac344 100644 --- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift +++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift @@ -65,6 +65,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { private var exploreRoomCoordinator: ExploreRoomCoordinator? private var membersCoordinator: SpaceMembersCoordinator? private var createSpaceCoordinator: SpaceCreationCoordinator? + private var createRoomCoordinator: CreateRoomCoordinator? // MARK: Public @@ -288,6 +289,18 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { self.createSpaceCoordinator = coordinator } + private func showAddRoom(spaceId: String, session: MXSession) { + let space = session.spaceService.getSpace(withId: spaceId) + let createRoomCoordinator = CreateRoomCoordinator(parameters: CreateRoomCoordinatorParameter(session: session, parentSpace: space)) + createRoomCoordinator.delegate = self + let presentable = createRoomCoordinator.toPresentable() + presentable.presentationController?.delegate = self + toPresentable().present(presentable, animated: true, completion: nil) + createRoomCoordinator.start() + self.add(childCoordinator: createRoomCoordinator) + self.createRoomCoordinator = createRoomCoordinator + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -380,7 +393,15 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate { case .exploreMembers: self.showMembers(spaceId: spaceId, session: session) case .addRoom: - AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddRoom, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName)) + session.spaceService.getSpace(withId: spaceId)?.canAddRoom { canAddRoom in + if canAddRoom { + self.showAddRoom(spaceId: spaceId, session: session) + } else { + let alert = UIAlertController(title: VectorL10n.spacesAddRoom, message: VectorL10n.spacesAddRoomMissingPermissionMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil)) + self.toPresentable().present(alert, animated: true, completion: nil) + } + } case .addSpace: AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName)) case .settings: @@ -423,6 +444,42 @@ extension SideMenuCoordinator: SpaceMembersCoordinatorDelegate { } } +// MARK: - CreateRoomCoordinatorDelegate +extension SideMenuCoordinator: CreateRoomCoordinatorDelegate { + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) { + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + self.createRoomCoordinator = nil + self.parameters.appNavigator.sideMenu.dismiss(animated: true) { + + } + if let spaceId = coordinator.parentSpace?.spaceId { + self.parameters.appNavigator.navigate(to: .space(spaceId)) + } + } + } + + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) { + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + self.createRoomCoordinator = nil + self.parameters.appNavigator.sideMenu.dismiss(animated: true) { + + } + if let spaceId = coordinator.parentSpace?.spaceId { + self.parameters.appNavigator.navigate(to: .space(spaceId)) + } + } + } + + func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) { + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + self.createRoomCoordinator = nil + } + } +} + // MARK: - UIAdaptivePresentationControllerDelegate extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate { @@ -430,5 +487,6 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate { self.exploreRoomCoordinator = nil self.membersCoordinator = nil self.createSpaceCoordinator = nil + self.createRoomCoordinator = nil } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinator.swift index 9607d9e869..39c4b4d74b 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinator.swift @@ -27,6 +27,7 @@ final class SpaceExploreRoomCoordinator: SpaceExploreRoomCoordinatorType { private var spaceExploreRoomViewModel: SpaceExploreRoomViewModelType private let spaceExploreRoomViewController: SpaceExploreRoomViewController + private let parameters: SpaceExploreRoomCoordinatorParameters // MARK: Public @@ -42,6 +43,7 @@ final class SpaceExploreRoomCoordinator: SpaceExploreRoomCoordinatorType { let spaceExploreRoomViewController = SpaceExploreRoomViewController.instantiate(with: spaceExploreRoomViewModel) self.spaceExploreRoomViewModel = spaceExploreRoomViewModel self.spaceExploreRoomViewController = spaceExploreRoomViewController + self.parameters = parameters } // MARK: - Public methods @@ -53,6 +55,10 @@ final class SpaceExploreRoomCoordinator: SpaceExploreRoomCoordinatorType { func toPresentable() -> UIViewController { return self.spaceExploreRoomViewController } + + func reloadRooms() { + spaceExploreRoomViewModel.process(viewAction: .reloadData) + } } // MARK: - SpaceExploreRoomViewModelCoordinatorDelegate @@ -64,4 +70,25 @@ extension SpaceExploreRoomCoordinator: SpaceExploreRoomViewModelCoordinatorDeleg func spaceExploreRoomViewModelDidCancel(_ viewModel: SpaceExploreRoomViewModelType) { self.delegate?.spaceExploreRoomCoordinatorDidCancel(self) } + + func spaceExploreRoomViewModelDidAddRoom(_ viewModel: SpaceExploreRoomViewModelType) { + guard let space = parameters.session.spaceService.getSpace(withId: parameters.spaceId) else { + showAddRoomMissingPermissionAlert() + return + } + + space.canAddRoom { canAddRoom in + if canAddRoom { + self.delegate?.spaceExploreRoomCoordinatorDidAddRoom(self) + } else { + self.showAddRoomMissingPermissionAlert() + } + } + } + + private func showAddRoomMissingPermissionAlert() { + let alert = UIAlertController(title: VectorL10n.spacesAddRoom, message: VectorL10n.spacesAddRoomMissingPermissionMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil)) + self.toPresentable().present(alert, animated: true, completion: nil) + } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorParameters.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorParameters.swift index cc3c97f76f..3e291f5b3b 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorParameters.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorParameters.swift @@ -20,4 +20,5 @@ struct SpaceExploreRoomCoordinatorParameters { let session: MXSession let spaceId: String let spaceName: String? + let showCancelMenuItem: Bool } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorType.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorType.swift index b092b17d6f..cdeaf71ec6 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorType.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomCoordinatorType.swift @@ -21,6 +21,7 @@ import Foundation protocol SpaceExploreRoomCoordinatorDelegate: AnyObject { func spaceExploreRoomCoordinator(_ coordinator: SpaceExploreRoomCoordinatorType, didSelect item: SpaceExploreRoomListItemViewData, from sourceView: UIView?) func spaceExploreRoomCoordinatorDidCancel(_ coordinator: SpaceExploreRoomCoordinatorType) + func spaceExploreRoomCoordinatorDidAddRoom(_ coordinator: SpaceExploreRoomCoordinatorType) } /// `SpaceExploreRoomCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewAction.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewAction.swift index aa1e67dc05..3aae0f838f 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewAction.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewAction.swift @@ -20,8 +20,11 @@ import Foundation /// SpaceExploreRoomViewController view actions exposed to view model enum SpaceExploreRoomViewAction { + case reloadData case loadData case complete(_ selectedItem: SpaceExploreRoomListItemViewData, _ sourceView: UIView?) case searchChanged(_ text: String?) + case join case cancel + case addRoom } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index a56b6ff758..21a246cf92 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -125,12 +125,14 @@ final class SpaceExploreRoomViewController: UIViewController { } private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() + if viewModel.showCancelMenuItem { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.leftBarButtonItem = cancelBarButtonItem } - - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - + self.vc_removeBackTitle() self.titleView = MainTitleView() @@ -145,6 +147,18 @@ final class SpaceExploreRoomViewController: UIViewController { self.setupTableViewHeader() } + private func setupJoinButton(canJoin: Bool) { + if canJoin { + let joinButtonItem = MXKBarButtonItem(title: VectorL10n.join, style: .done) { [weak self] in + self?.viewModel.process(viewAction: .join) + } + + self.navigationItem.rightBarButtonItem = joinButtonItem + } else { + self.navigationItem.rightBarButtonItem = nil + } + } + private func setupTableViewHeader() { addRoomHeaderView.delegate = self tableView.tableHeaderView = addRoomHeaderView @@ -176,6 +190,8 @@ final class SpaceExploreRoomViewController: UIViewController { self.renderEmptyFilterResult() case .error(let error): self.render(error: error) + case .canJoin(let canJoin): + self.setupJoinButton(canJoin: canJoin) } } @@ -207,10 +223,6 @@ final class SpaceExploreRoomViewController: UIViewController { self.viewModel.process(viewAction: .cancel) } - @objc private func addRoomAction(semder: UIView) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) - } - // MARK: - UISearchBarDelegate override func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { @@ -274,7 +286,7 @@ extension SpaceExploreRoomViewController: SpaceExploreRoomViewModelViewDelegate extension SpaceExploreRoomViewController: AddItemHeaderViewDelegate { func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) + self.viewModel.process(viewAction: .addRoom) } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift index c9f54491c0..828c63a66c 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift @@ -17,6 +17,7 @@ */ import Foundation +import MatrixSDK final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { @@ -32,6 +33,11 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { private var nextBatch: String? private var rootSpaceChildInfo: MXSpaceChildInfo? + private var canJoin: Bool = false { + didSet { + self.update(viewState: .canJoin(self.canJoin)) + } + } private var itemDataList: [SpaceExploreRoomListItemViewData] = [] { didSet { self.updateFilteredItemList() @@ -51,22 +57,29 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { self.update(viewState: .emptyFilterResult) } } else { - self.update(viewState: .loaded(self.filteredItemDataList, self.nextBatch != nil && (self.searchKeyword ?? "").isEmpty)) + self.update(viewState: .loaded(self.filteredItemDataList, self.hasMore)) } } } + private var hasMore: Bool { + self.nextBatch != nil && (self.searchKeyword ?? "").isEmpty + } + + private var spaceGraphObserver: Any? // MARK: Public weak var viewDelegate: SpaceExploreRoomViewModelViewDelegate? weak var coordinatorDelegate: SpaceExploreRoomViewModelCoordinatorDelegate? - + private(set) var showCancelMenuItem: Bool + // MARK: - Setup init(parameters: SpaceExploreRoomCoordinatorParameters) { self.session = parameters.session self.spaceId = parameters.spaceId self.spaceName = parameters.spaceName + self.showCancelMenuItem = parameters.showCancelMenuItem } deinit { @@ -79,6 +92,9 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { switch viewAction { case .loadData: self.loadData() + case .reloadData: + self.nextBatch = nil + self.loadData() case .complete(let selectedItem, let sourceView): self.coordinatorDelegate?.spaceExploreRoomViewModel(self, didSelect: selectedItem, from: sourceView) case .cancel: @@ -86,6 +102,10 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { self.coordinatorDelegate?.spaceExploreRoomViewModelDidCancel(self) case .searchChanged(let newText): self.searchKeyword = newText + case .addRoom: + self.coordinatorDelegate?.spaceExploreRoomViewModelDidAddRoom(self) + case .join: + self.joinSpace() } } @@ -104,6 +124,8 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { self.update(viewState: .loading) } + self.canJoin = self.session.room(withRoomId: spaceId) == nil + self.currentOperation = self.session.spaceService.getSpaceChildrenForSpace(withId: self.spaceId, suggestedOnly: false, limit: nil, maxDepth: 1, paginationToken: self.nextBatch, completion: { [weak self] response in guard let self = self else { return @@ -111,7 +133,9 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { switch response { case .success(let spaceSummary): + let appendData = self.nextBatch != nil self.nextBatch = spaceSummary.nextBatch + // The MXSpaceChildInfo of the root space is available only in the first batch if let rootSpaceInfo = spaceSummary.spaceInfo { self.rootSpaceChildInfo = rootSpaceInfo @@ -131,7 +155,12 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { }).sorted(by: { item1, item2 in return !item2.childInfo.suggested || item1.childInfo.suggested }) - self.itemDataList.append(contentsOf: batchedItemDataList) + + if appendData { + self.itemDataList.append(contentsOf: batchedItemDataList) + } else { + self.itemDataList = batchedItemDataList + } case .failure(let error): self.update(viewState: .error(error)) } @@ -146,6 +175,9 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { private func cancelOperations() { self.currentOperation?.cancel() + if let observer = self.spaceGraphObserver { + NotificationCenter.default.removeObserver(observer) + } } private func updateFilteredItemList() { @@ -158,4 +190,26 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { return (itemData.childInfo.name?.lowercased().contains(searchKeyword) ?? false) || (itemData.childInfo.topic?.lowercased().contains(searchKeyword) ?? false) }) } + + private func joinSpace() { + self.update(viewState: .loading) + + self.currentOperation = session.joinRoom(spaceId) { [weak self] response in + switch response { + case .success: + self?.spaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main, using: { [weak self] notification in + guard let self = self else { return } + + self.currentOperation = nil + if let observer = self.spaceGraphObserver { + NotificationCenter.default.removeObserver(observer) + } + self.canJoin = false + self.update(viewState: .loaded(self.filteredItemDataList, self.hasMore)) + }) + case .failure(let error): + self?.update(viewState: .error(error)) + } + } + } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift index a5e463c75e..56d236200b 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift @@ -25,6 +25,7 @@ protocol SpaceExploreRoomViewModelViewDelegate: AnyObject { protocol SpaceExploreRoomViewModelCoordinatorDelegate: AnyObject { func spaceExploreRoomViewModel(_ viewModel: SpaceExploreRoomViewModelType, didSelect item: SpaceExploreRoomListItemViewData, from sourceView: UIView?) func spaceExploreRoomViewModelDidCancel(_ viewModel: SpaceExploreRoomViewModelType) + func spaceExploreRoomViewModelDidAddRoom(_ viewModel: SpaceExploreRoomViewModelType) } /// Protocol describing the view model used by `SpaceExploreRoomViewController` @@ -32,6 +33,7 @@ protocol SpaceExploreRoomViewModelType { var viewDelegate: SpaceExploreRoomViewModelViewDelegate? { get set } var coordinatorDelegate: SpaceExploreRoomViewModelCoordinatorDelegate? { get set } - + var showCancelMenuItem: Bool { get } + func process(viewAction: SpaceExploreRoomViewAction) } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewState.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewState.swift index 52203c1906..51f784596c 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewState.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewState.swift @@ -23,6 +23,7 @@ enum SpaceExploreRoomViewState { case loading case spaceNameFound(_ spaceName: String) case loaded(_ children: [SpaceExploreRoomListItemViewData], _ hasMore: Bool) + case canJoin(Bool) case emptySpace case emptyFilterResult case error(Error) diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index c00b0115c9..82c207ecc8 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { +final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { // MARK: - Properties @@ -31,6 +31,7 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { // We need to stack the ID of visited space and subspaces so we know what is the current space ID when navigating to a room private var spaceIdStack: [String] private weak var roomDetailCoordinator: SpaceChildRoomDetailCoordinator? + private weak var currentExploreRoomCoordinator: SpaceExploreRoomCoordinator? private lazy var slidingModalPresenter: SlidingModalPresenter = { return SlidingModalPresenter() @@ -61,6 +62,7 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { rootCoordinator.start() self.add(childCoordinator: rootCoordinator) + self.currentExploreRoomCoordinator = rootCoordinator self.navigationRouter.setRootModule(rootCoordinator) } @@ -74,8 +76,12 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { private func pushSpace(with item: SpaceExploreRoomListItemViewData) { let coordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: item.childInfo.childRoomId, spaceName: item.childInfo.name) coordinator.start() + self.add(childCoordinator: coordinator) + self.currentExploreRoomCoordinator = coordinator + self.spaceIdStack.append(item.childInfo.childRoomId) + self.navigationRouter.push(coordinator.toPresentable(), animated: true) { self.remove(childCoordinator: coordinator) self.spaceIdStack.removeLast() @@ -118,7 +124,7 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { } private func createShowSpaceExploreRoomCoordinator(session: MXSession, spaceId: String, spaceName: String?) -> SpaceExploreRoomCoordinator { - let coordinator = SpaceExploreRoomCoordinator(parameters: SpaceExploreRoomCoordinatorParameters(session: session, spaceId: spaceId, spaceName: spaceName)) + let coordinator = SpaceExploreRoomCoordinator(parameters: SpaceExploreRoomCoordinatorParameters(session: session, spaceId: spaceId, spaceName: spaceName, showCancelMenuItem: self.navigationRouter.modules.isEmpty)) coordinator.delegate = self return coordinator } @@ -145,6 +151,17 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType { roomViewController.showMissedDiscussionsBadge = false }) } + + private func presentRoomCreation() { + let space = session.spaceService.getSpace(withId: spaceIdStack.last ?? "") + let createRoomCoordinator = CreateRoomCoordinator(parameters: CreateRoomCoordinatorParameter(session: self.session, parentSpace: space)) + createRoomCoordinator.delegate = self + let presentable = createRoomCoordinator.toPresentable() + presentable.presentationController?.delegate = self + toPresentable().present(presentable, animated: true, completion: nil) + createRoomCoordinator.start() + self.add(childCoordinator: createRoomCoordinator) + } } // MARK: - ShowSpaceExploreRoomCoordinatorDelegate @@ -160,6 +177,10 @@ extension ExploreRoomCoordinator: SpaceExploreRoomCoordinatorDelegate { func spaceExploreRoomCoordinatorDidCancel(_ coordinator: SpaceExploreRoomCoordinatorType) { self.delegate?.exploreRoomCoordinatorDidComplete(self) } + + func spaceExploreRoomCoordinatorDidAddRoom(_ coordinator: SpaceExploreRoomCoordinatorType) { + self.presentRoomCreation() + } } // MARK: - ShowSpaceChildRoomDetailCoordinator @@ -189,3 +210,41 @@ extension ExploreRoomCoordinator: SpaceChildRoomDetailCoordinatorDelegate { } } } + +// MARK: - CreateRoomCoordinatorDelegate +extension ExploreRoomCoordinator: CreateRoomCoordinatorDelegate { + + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) { + self.currentExploreRoomCoordinator?.reloadRooms() + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + self.navigateTo(roomWith: room.roomId) + } + } + + func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) { + self.currentExploreRoomCoordinator?.reloadRooms() + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + } + } + + func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) { + coordinator.toPresentable().dismiss(animated: true) { + self.remove(childCoordinator: coordinator) + } + } + +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ExploreRoomCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + guard let lastCoordinator = childCoordinators.last else { + return + } + self.remove(childCoordinator: lastCoordinator) + } + +} diff --git a/Riot/Routers/TabRouters/SegmentedController.storyboard b/Riot/Routers/TabRouters/SegmentedController.storyboard new file mode 100644 index 0000000000..a723e4c31c --- /dev/null +++ b/Riot/Routers/TabRouters/SegmentedController.storyboard @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Routers/TabRouters/SegmentedController.swift b/Riot/Routers/TabRouters/SegmentedController.swift new file mode 100644 index 0000000000..07ec308e5b --- /dev/null +++ b/Riot/Routers/TabRouters/SegmentedController.swift @@ -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 Foundation + +struct SegmentedControllerTab { + let title: String? + let viewController: UIViewController +} + +class SegmentedController: UIViewController, Themable { + + // MARK: Outlets + + @IBOutlet private var segmentedControl: UISegmentedControl! + @IBOutlet private var contentView: UIView! + + // MARK: Private + + private var currentController: UIViewController? { + didSet { + if let viewController = oldValue { + viewController.vc_removeFromParent(animated: true) + } + + if let viewController = currentController { + vc_addChildViewController(viewController: viewController, onView: contentView, animated: true) + + // Needed for swiftUI as navigation items are loaded while loading the view + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.navigationItem.rightBarButtonItem = viewController.navigationItem.rightBarButtonItem + self.navigationItem.rightBarButtonItems = viewController.navigationItem.rightBarButtonItems + self.navigationItem.leftBarButtonItem = viewController.navigationItem.leftBarButtonItem + self.navigationItem.leftBarButtonItems = viewController.navigationItem.leftBarButtonItems + if self.title == nil { + self.navigationItem.title = viewController.navigationItem.title + self.navigationItem.titleView = viewController.navigationItem.titleView + } + } + } + } + } + private var theme: Theme! + + // MARK: Properties + + var tabs: [SegmentedControllerTab] = [] { + didSet { + if isViewLoaded { + populateSegmentedControl() + } + } + } + + // MARK: Setup + + class func instantiate() -> SegmentedController { + let storyboard = UIStoryboard(name: "SegmentedController", bundle: nil) + let viewController = storyboard.instantiateInitialViewController() as? SegmentedController ?? SegmentedController() + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + populateSegmentedControl() + update(theme: theme) + registerThemeServiceDidChangeThemeNotification() + } + + // MARK: Actions + + @IBAction private func segmentDidChange(sender: UISegmentedControl) { + self.currentController = tabs[sender.selectedSegmentIndex].viewController + } + + // MARK: Themable + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.theme = ThemeService.shared().theme + self.update(theme: self.theme) + } + + func update(theme: Theme) { + self.view.backgroundColor = theme.colors.navigation + self.contentView.backgroundColor = theme.colors.navigation + } + + // MARK: Private + + private func populateSegmentedControl() { + segmentedControl.removeAllSegments() + for tab in tabs { + let title = tab.title ?? tab.viewController.tabBarItem.title ?? tab.viewController.title + segmentedControl.insertSegment(withTitle: title, at: segmentedControl.numberOfSegments, animated: false) + } + + if segmentedControl.numberOfSegments > 0 { + segmentedControl.selectedSegmentIndex = 0 + self.currentController = tabs.first?.viewController + } + } + +} diff --git a/Riot/Routers/TabRouters/SegmentedRouter.swift b/Riot/Routers/TabRouters/SegmentedRouter.swift new file mode 100644 index 0000000000..6cf7bf8ca9 --- /dev/null +++ b/Riot/Routers/TabRouters/SegmentedRouter.swift @@ -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 + +class SegmentedRouter: NSObject, TabbedRouterType { + + // MARK: - Private + + let segmentedController: SegmentedController + + /// Returns the presentables associated to each view controller + var tabs: [TabbedRouterTab] = [] { + didSet { + segmentedController.tabs = tabs.compactMap({ tab in + let viewController = tab.module.toPresentable() + + guard viewController is UITabBarController == false else { + return nil + } + + return SegmentedControllerTab(title: tab.title, viewController: viewController) + }) + } + } + + init(segmentedController: SegmentedController = SegmentedController.instantiate()) { + self.segmentedController = segmentedController + } + + // MARK: - Public + + func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) { + MXLog.debug("[SegmentedRouter] Present \(module)") + segmentedController.present(module.toPresentable(), animated: animated, completion: nil) + } + + func dismissModule(animated: Bool, completion: (() -> Void)?) { + MXLog.debug("[SegmentedRouter] dismiss") + segmentedController.dismiss(animated: animated, completion: completion) + } + + func toPresentable() -> UIViewController { + return segmentedController + } + + +} diff --git a/Riot/Routers/TabRouters/TabBarRouter.swift b/Riot/Routers/TabRouters/TabBarRouter.swift new file mode 100644 index 0000000000..356b6420de --- /dev/null +++ b/Riot/Routers/TabRouters/TabBarRouter.swift @@ -0,0 +1,107 @@ +// +// 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 WeakDictionary + +class TabBarRouter: NSObject, TabbedRouterType { + + // MARK: - Properties + + // MARK: Private + + private let tabBarController: UITabBarController + + /// Stores the association between the added Presentable and his view controller. + /// They can be the same if the controller is not added via his Coordinator or it is a simple UIViewController. + private var storedModules = WeakDictionary() + + // MARK: Public + + /// Returns the presentables associated to each view controller + var tabs: [TabbedRouterTab] = [] { + didSet { + guard !tabs.isEmpty else { + tabBarController.viewControllers = nil + return + } + + tabBarController.viewControllers = tabs.compactMap({ tab in + let viewController = tab.module.toPresentable() + + guard viewController is UITabBarController == false else { + return nil + } + + if tab.title != nil || tab.icon != nil { + viewController.tabBarItem.title = tab.title + viewController.tabBarItem.image = tab.icon + } + + return viewController + }) + } + } + + /// Return the view controllers stack + var viewControllers: [UIViewController] { + return tabBarController.viewControllers ?? [] + } + + // MARK: - Setup + + init(tabBarController: UITabBarController = UITabBarController(), tabs: [TabbedRouterTab]? = nil) { + self.tabBarController = tabBarController + super.init() + self.tabBarController.delegate = self + + if let tabs = tabs { + self.tabs = tabs + } + } + + // MARK: - Public + + func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) { + MXLog.debug("[TabBarRouter] Present \(module)") + tabBarController.present(module.toPresentable(), animated: animated, completion: nil) + } + + func dismissModule(animated: Bool, completion: (() -> Void)?) { + tabBarController.dismiss(animated: animated, completion: completion) + } + + // MARK: Presentable + + func toPresentable() -> UIViewController { + return tabBarController + } + + // MARK: - Private + + private func module(for viewController: UIViewController) -> Presentable { + + guard let module = self.storedModules[viewController] as? Presentable else { + return viewController + } + return module + } + +} + +extension TabBarRouter: UITabBarControllerDelegate { + +} diff --git a/Riot/Routers/TabRouters/TabbedRouterType.swift b/Riot/Routers/TabRouters/TabbedRouterType.swift new file mode 100644 index 0000000000..6c3279d1e1 --- /dev/null +++ b/Riot/Routers/TabRouters/TabbedRouterType.swift @@ -0,0 +1,44 @@ +// +// 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 + +struct TabbedRouterTab { + let title: String? + let icon: UIImage? + let module: Presentable +} + +/// Protocol describing a router that wraps the root navigation of the application. +/// Routers are used to be passed between coordinators. They handles only `physical` navigation. +protocol TabbedRouterType: AnyObject, Presentable { + var tabs: [TabbedRouterTab] { get set } + + /// Present modally a view controller on the root view controller + /// + /// - Parameters: + /// - module: Specify true to animate the transition. + /// - animated: Specify true to animate the transition. + /// - completion: Animation completion. + func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) + + /// Dismiss modally presented view controller from root view controller + /// + /// - Parameters: + /// - animated: Specify true to animate the transition. + /// - completion: Animation completion. + func dismissModule(animated: Bool, completion: (() -> Void)?) +} diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 3a8bac1b46..a53c6d67d4 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -26,7 +26,7 @@ enum MockAppScreens { MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, MockSpaceCreationEmailInvitesScreenState.self, - MockSpaceCreationMatrixItemChooserScreenState.self, + MockMatrixItemChooserScreenState.self, MockSpaceCreationMenuScreenState.self, MockSpaceCreationRoomsScreenState.self, MockSpaceCreationSettingsScreenState.self, diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateAction.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift similarity index 68% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateAction.swift rename to RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift index daa989ccfe..ed0ac34566 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateAction.swift +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift @@ -14,10 +14,11 @@ // limitations under the License. // -import Foundation +import SwiftUI -/// Actions to be performed on the `ViewModel` State -enum SpaceCreationMatrixItemListStateAction { - case updateItems([SpaceCreationMatrixItem]) - case updateSelection(Set) +class AddRoomSelectorViewProvider: MatrixItemChooserCoordinatorViewProvider { + @available(iOS 14, *) + func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { + return AnyView(AddRoomSelector(viewModel: viewModel)) + } } diff --git a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift new file mode 100644 index 0000000000..9e194ff7f8 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift @@ -0,0 +1,72 @@ +// +// 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 AddRoomItemsProcessor: MatrixItemChooserProcessorProtocol { + + // MARK: Private + + private let parentSpace: MXSpace + + // MARK: Setup + + init(parentSpace: MXSpace) { + self.parentSpace = parentSpace + } + + // MARK: MatrixItemChooserSelectionProcessorProtocol + + var dataType: MatrixItemChooserType { + .room + } + + func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) { + addChild(from: itemsIds, at: 0, completion: completion) + } + + func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { + return !parentSpace.isRoomAChild(roomId: item.id) + } + + // MARK: Private + + /// Add room with roomId from list of room IDs at index to the parentSpace. + /// Recurse to the next index once done. + func addChild(from roomIds: [String], at index: Int, completion: @escaping (Result) -> Void) { + guard index < roomIds.count else { + // last item has been processed or list is empty --> the recursion has finished + completion(Result.success(())) + return + } + + let roomId = roomIds[index] + + guard !parentSpace.isRoomAChild(roomId: roomId) else { + addChild(from: roomIds, at: index + 1, completion: completion) + return + } + + parentSpace.addChild(roomId: roomIds[index]) { [weak self] response in + switch response { + case .success: + self?.addChild(from: roomIds, at: index + 1, completion: completion) + case .failure(let error): + completion(Result.failure(error)) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift new file mode 100644 index 0000000000..289a1d564a --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift @@ -0,0 +1,58 @@ +// +// 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 AddRoomSelector: View { + + // MARK: Properties + + @ObservedObject var viewModel: MatrixItemChooserViewModel.Context + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Setup + + var body: some View { + MatrixItemChooser(viewModel: viewModel) + .background(theme.colors.background) + .navigationBarItems(leading: cancelButton, + trailing: doneButton) + } + + // MARK: Private + + private var cancelButton: some View { + Button(VectorL10n.cancel, action: { + viewModel.send(viewAction: .cancel) + }) + .font(theme.fonts.body) + .foregroundColor(theme.colors.accent) + } + + private var doneButton: some View { + Button(VectorL10n.add, action: { + viewModel.send(viewAction: .done) + }) + .font(theme.fonts.body) + .foregroundColor(viewModel.viewState.selectedItemIds.isEmpty ? theme.colors.quarterlyContent : theme.colors.accent) + .opacity(viewModel.viewState.selectedItemIds.isEmpty ? 0.7 : 1) + .disabled(viewModel.viewState.selectedItemIds.isEmpty) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift new file mode 100644 index 0000000000..95b1d0472b --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift @@ -0,0 +1,100 @@ +// +// 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 UIKit +import SwiftUI + +@available(iOS 14.0, *) +internal protocol MatrixItemChooserCoordinatorViewProvider { + func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView +} + +@available(iOS 14.0, *) +struct MatrixItemChooserCoordinatorParameters { + let session: MXSession + let title: String? + let detail: String? + let selectedItemsIds: [String] + let viewProvider: MatrixItemChooserCoordinatorViewProvider? + let itemsProcessor: MatrixItemChooserProcessorProtocol? + + init(session: MXSession, + title: String? = nil, + detail: String? = nil, + selectedItemsIds: [String] = [], + viewProvider: MatrixItemChooserCoordinatorViewProvider? = nil, + itemsProcessor: MatrixItemChooserProcessorProtocol?) { + self.session = session + self.title = title + self.detail = detail + self.selectedItemsIds = selectedItemsIds + self.viewProvider = viewProvider + self.itemsProcessor = itemsProcessor + } +} + +@available(iOS 14.0.0, *) +final class MatrixItemChooserCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: MatrixItemChooserCoordinatorParameters + private let matrixItemChooserHostingController: UIViewController + private var matrixItemChooserViewModel: MatrixItemChooserViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((MatrixItemChooserViewModelResult) -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: MatrixItemChooserCoordinatorParameters) { + self.parameters = parameters + let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail) + matrixItemChooserViewModel = viewModel + if let viewProvider = parameters.viewProvider { + let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + matrixItemChooserHostingController = VectorHostingController(rootView: view) + } else { + let view = MatrixItemChooser(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + matrixItemChooserHostingController = VectorHostingController(rootView: view) + } + } + + // MARK: - Coordinator + + func start() { + MXLog.debug("[MatrixItemChooserCoordinator] did start.") + matrixItemChooserViewModel.completion = { [weak self] result in + MXLog.debug("[MatrixItemChooserCoordinator] MatrixItemChooserViewModel did complete with result: \(result).") + guard let self = self else { return } + self.completion?(result) + } + } + + // MARK: - Presentable + + func toPresentable() -> UIViewController { + return self.matrixItemChooserHostingController + } +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift new file mode 100644 index 0000000000..d228eaf911 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift @@ -0,0 +1,68 @@ +// +// 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 MatrixItemChooserType { + case room + case people +} + +// MARK: View model + +enum MatrixItemChooserStateAction { + case loadingState(Bool) + case updateError(Error?) + case updateItems([MatrixListItemData]) + case updateSelection(Set) +} + +enum MatrixItemChooserViewModelResult { + case cancel + case done([String]) + case back +} + +// MARK: View + +struct MatrixListItemData { + let id: String + let avatar: AvatarInput + let displayName: String? + let detailText: String? +} + +extension MatrixListItemData: Identifiable, Equatable {} + +struct MatrixItemChooserViewState: BindableState { + var title: String? + var message: String? + var emptyListMessage: String + var items: [MatrixListItemData] + var selectedItemIds: Set + var loading: Bool + var error: String? +} + +enum MatrixItemChooserViewAction { + case searchTextChanged(String) + case itemTapped(_ itemId: String) + case done + case cancel + case back +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift new file mode 100644 index 0000000000..9de46349a5 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift @@ -0,0 +1,127 @@ +// +// 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 MatrixItemChooserViewModelType = StateStoreViewModel +@available(iOS 14, *) +class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChooserViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private var matrixItemChooserService: MatrixItemChooserServiceProtocol + + // MARK: Public + + var completion: ((MatrixItemChooserViewModelResult) -> Void)? + + // MARK: - Setup + + static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol { + return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail) + } + + private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) { + self.matrixItemChooserService = matrixItemChooserService + super.init(initialViewState: Self.defaultState(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail)) + startObservingItems() + } + + private static func defaultState(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewState { + let title = title + let message = detail + let emptyListMessage = VectorL10n.spacesNoResultFoundTitle + + return MatrixItemChooserViewState(title: title, message: message, emptyListMessage: emptyListMessage, items: matrixItemChooserService.itemsSubject.value, selectedItemIds: matrixItemChooserService.selectedItemIdsSubject.value, loading: false) + } + + private func startObservingItems() { + let itemsUpdatePublisher = matrixItemChooserService.itemsSubject + .map(MatrixItemChooserStateAction.updateItems) + .eraseToAnyPublisher() + dispatch(actionPublisher: itemsUpdatePublisher) + + let selectionPublisher = matrixItemChooserService.selectedItemIdsSubject + .map(MatrixItemChooserStateAction.updateSelection) + .eraseToAnyPublisher() + dispatch(actionPublisher: selectionPublisher) + } + + // MARK: - Public + + override func process(viewAction: MatrixItemChooserViewAction) { + switch viewAction { + case .cancel: + cancel() + case .back: + back() + case .done: + dispatch(action: .loadingState(true)) + matrixItemChooserService.processSelection { [weak self] result in + guard let self = self else { return } + + self.dispatch(action: .loadingState(false)) + + switch result { + case .success: + let selectedItemsId = Array(self.matrixItemChooserService.selectedItemIdsSubject.value) + self.done(selectedItemsId: selectedItemsId) + case .failure(let error): + self.matrixItemChooserService.refresh() + self.dispatch(action: .updateError(error)) + } + } + case .searchTextChanged(let searchText): + self.matrixItemChooserService.searchText = searchText + case .itemTapped(let itemId): + self.matrixItemChooserService.reverseSelectionForItem(withId: itemId) + } + } + + override class func reducer(state: inout MatrixItemChooserViewState, action: MatrixItemChooserStateAction) { + switch action { + case .updateItems(let items): + state.items = items + case .updateSelection(let selectedItemIds): + state.selectedItemIds = selectedItemIds + case .loadingState(let loading): + state.loading = loading + state.error = nil + case .updateError(let error): + state.error = error?.localizedDescription + } + UILog.debug("[MatrixItemChooserViewModel] reducer with action \(action) produced state: \(state)") + } + + private func done(selectedItemsId: [String]) { + completion?(.done(selectedItemsId)) + } + + private func cancel() { + completion?(.cancel) + } + + private func back() { + completion?(.back) + } + +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift similarity index 59% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinatorParameters.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift index 638c48ed19..d91b21acab 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -18,8 +16,11 @@ import Foundation -struct SpaceCreationMatrixItemChooserCoordinatorParameters { - let session: MXSession - let type: SpaceCreationMatrixItemType - let creationParams: SpaceCreationParameters +protocol MatrixItemChooserViewModelProtocol { + + var completion: ((MatrixItemChooserViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol + @available(iOS 14, *) + var context: MatrixItemChooserViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift similarity index 61% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserScreenState.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index db48f63505..5bff44b45c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -22,7 +20,7 @@ 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 MockSpaceCreationMatrixItemChooserScreenState: MockScreenState, CaseIterable { +enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { // A case for each state you want to represent // with specific, minimal associated data that will allow you // mock that screen. @@ -32,27 +30,27 @@ enum MockSpaceCreationMatrixItemChooserScreenState: MockScreenState, CaseIterabl /// The associated screen var screenType: Any.Type { - SpaceCreationMatrixItem.self + MatrixItemChooserType.self } /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let service: MockSpaceCreationMatrixItemChooserService + let service: MockMatrixItemChooserService switch self { case .noItems: - service = MockSpaceCreationMatrixItemChooserService(type: .room, items: []) + service = MockMatrixItemChooserService(type: .room, items: []) case .items: - service = MockSpaceCreationMatrixItemChooserService() + service = MockMatrixItemChooserService() case .selectedItems: - service = MockSpaceCreationMatrixItemChooserService(type: .room, items: MockSpaceCreationMatrixItemChooserService.mockItems, selectedItemIndexes: [0, 2]) + service = MockMatrixItemChooserService(type: .room, items: MockMatrixItemChooserService.mockItems, selectedItemIndexes: [0, 2]) } - let viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: SpaceCreationParameters()) + let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: "Some title", detail: "Detail text describing the current screen") // can simulate service and viewModel actions here if needs be. return ( [service, viewModel], - AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel.context) + AnyView(MatrixItemChooser(viewModel: viewModel.context) .addDependency(MockAvatarService.example)) ) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/SpaceCreationMatrixItemChooserServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift similarity index 70% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/SpaceCreationMatrixItemChooserServiceProtocol.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift index 66c5333251..6993e1bd9e 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/SpaceCreationMatrixItemChooserServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -20,11 +18,13 @@ import Foundation import Combine @available(iOS 14.0, *) -protocol SpaceCreationMatrixItemChooserServiceProtocol { - var type: SpaceCreationMatrixItemType { get } - var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never> { get } +protocol MatrixItemChooserServiceProtocol { + var type: MatrixItemChooserType { get } + var itemsSubject: CurrentValueSubject<[MatrixListItemData], Never> { get } var selectedItemIdsSubject: CurrentValueSubject, Never> { get } var searchText: String { get set } func reverseSelectionForItem(withId itemId: String) + func processSelection(completion: @escaping (Result) -> Void) + func refresh() } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationMatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift similarity index 56% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationMatrixItemChooserService.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift index cda1b485d9..4a1baa6c38 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationMatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift @@ -17,56 +17,52 @@ import Foundation import Combine +protocol MatrixItemChooserProcessorProtocol { + var dataType: MatrixItemChooserType { get } + func computeSelection(withIds itemsIds:[String], completion: @escaping (Result) -> Void) + func isItemIncluded(_ item: (MatrixListItemData)) -> Bool +} + @available(iOS 14.0, *) -class SpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol { +class MatrixItemChooserService: MatrixItemChooserServiceProtocol { // MARK: - Properties // MARK: Private - private let processingQueue = DispatchQueue(label: "io.element.SpaceCreationMatrixItemChooserService.processingQueue") + private let processingQueue = DispatchQueue(label: "io.element.element.MatrixItemChooserService.processingQueue") private let completionQueue = DispatchQueue.main private let session: MXSession - private let items: [SpaceCreationMatrixItem] - private var filteredItems: [SpaceCreationMatrixItem] { + private let items: [MatrixListItemData] + private var filteredItems: [MatrixListItemData] { didSet { itemsSubject.send(filteredItems) } } private var selectedItemIds: Set + private let itemsProcessor: MatrixItemChooserProcessorProtocol? // MARK: Public - private(set) var type: SpaceCreationMatrixItemType - private(set) var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never> + private(set) var type: MatrixItemChooserType + private(set) var itemsSubject: CurrentValueSubject<[MatrixListItemData], Never> private(set) var selectedItemIdsSubject: CurrentValueSubject, Never> var searchText: String = "" { didSet { - if searchText.isEmpty { - filteredItems = items - } else { - self.processingQueue.async { - let lowercasedSearchText = self.searchText.lowercased() - let filteredItems = self.items.filter { $0.id.lowercased().contains(lowercasedSearchText) || ($0.displayName ?? "").lowercased().contains(lowercasedSearchText) } - - self.completionQueue.async { - self.filteredItems = filteredItems - } - } - } + refresh() } } // MARK: - Setup - init(session: MXSession, type: SpaceCreationMatrixItemType, selectedItemIds: [String]) { + init(session: MXSession, selectedItemIds: [String], itemsProcessor: MatrixItemChooserProcessorProtocol?) { self.session = session - self.type = type + self.type = itemsProcessor?.dataType ?? .room switch type { case .people: self.items = session.users().map { user in - SpaceCreationMatrixItem(mxUser: user) + MatrixListItemData(mxUser: user) } case .room: self.items = session.rooms.compactMap { room in @@ -74,14 +70,17 @@ class SpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServi return nil } - return SpaceCreationMatrixItem(mxRoom: room, spaceService: session.spaceService) + return MatrixListItemData(mxRoom: room, spaceService: session.spaceService) } } self.itemsSubject = CurrentValueSubject(self.items) - self.filteredItems = self.items + self.filteredItems = [] self.selectedItemIds = Set(selectedItemIds) self.selectedItemIdsSubject = CurrentValueSubject(self.selectedItemIds) + self.itemsProcessor = itemsProcessor + + refresh() } // MARK: - Public @@ -94,10 +93,54 @@ class SpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServi } selectedItemIdsSubject.send(selectedItemIds) } + + func processSelection(completion: @escaping (Result) -> Void) { + guard let selectionProcessor = self.itemsProcessor else { + completion(Result.success(())) + return + } + + selectionProcessor.computeSelection(withIds: Array(selectedItemIds), completion: completion) + } + + func refresh() { + self.processingQueue.async { [weak self] in + guard let self = self else { return } + let filteredItems = self.filter(items: self.items) + + self.completionQueue.async { + self.filteredItems = filteredItems + } + } + } + // MARK: - Private + + private func filter(items: [MatrixListItemData]) -> [MatrixListItemData] { + if searchText.isEmpty { + if let selectionProcessor = self.itemsProcessor { + return items.filter { + selectionProcessor.isItemIncluded($0) + } + } else { + return items + } + } else { + let lowercasedSearchText = self.searchText.lowercased() + if let selectionProcessor = self.itemsProcessor { + return items.filter { + selectionProcessor.isItemIncluded($0) && ($0.id.lowercased().contains(lowercasedSearchText) || ($0.displayName ?? "").lowercased().contains(lowercasedSearchText)) + } + } else { + return items.filter { + $0.id.lowercased().contains(lowercasedSearchText) || ($0.displayName ?? "").lowercased().contains(lowercasedSearchText) + } + } + } + } } -fileprivate extension SpaceCreationMatrixItem { +fileprivate extension MatrixListItemData { init(mxUser: MXUser) { self.init(id: mxUser.userId, avatar: mxUser.avatarData, displayName: mxUser.displayname, detailText: mxUser.userId) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift similarity index 63% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserService.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift index 7974f13047..17a92eda48 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/Mock/MockSpaceCreationMatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -20,20 +18,20 @@ import Foundation import Combine @available(iOS 14.0, *) -class MockSpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol { +class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { static let mockItems = [ - SpaceCreationMatrixItem(id: "!aaabaa:matrix.org", avatar: MockAvatarInput.example, displayName: "Matrix Discussion", detailText: "Descripton of this room"), - SpaceCreationMatrixItem(id: "!zzasds:matrix.org", avatar: MockAvatarInput.example, displayName: "Element Mobile", detailText: "Descripton of this room"), - SpaceCreationMatrixItem(id: "!scthve:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice Personal", detailText: "Descripton of this room") + MatrixListItemData(id: "!aaabaa:matrix.org", avatar: MockAvatarInput.example, displayName: "Matrix Discussion", detailText: "Descripton of this room"), + MatrixListItemData(id: "!zzasds:matrix.org", avatar: MockAvatarInput.example, displayName: "Element Mobile", detailText: "Descripton of this room"), + MatrixListItemData(id: "!scthve:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice Personal", detailText: "Descripton of this room") ] - var itemsSubject: CurrentValueSubject<[SpaceCreationMatrixItem], Never> + var itemsSubject: CurrentValueSubject<[MatrixListItemData], Never> var selectedItemIdsSubject: CurrentValueSubject, Never> var searchText: String = "" - var type: SpaceCreationMatrixItemType = .room + var type: MatrixItemChooserType = .room var selectedItemIds: Set = Set() - init(type: SpaceCreationMatrixItemType = .room, items: [SpaceCreationMatrixItem] = mockItems, selectedItemIndexes: [Int] = []) { + init(type: MatrixItemChooserType = .room, items: [MatrixListItemData] = mockItems, selectedItemIndexes: [Int] = []) { itemsSubject = CurrentValueSubject(items) var selectedItemIds = Set() for index in selectedItemIndexes { @@ -63,4 +61,12 @@ class MockSpaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserS } selectedItemIdsSubject.send(selectedItemIds) } + + func processSelection(completion: @escaping (Result) -> Void) { + completion(Result.success(())) + } + + func refresh() { + + } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/UI/SpaceCreationMatrixItemChooserUITests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift similarity index 79% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/UI/SpaceCreationMatrixItemChooserUITests.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift index e6368d86fe..e292151b91 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/UI/SpaceCreationMatrixItemChooserUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -20,18 +18,18 @@ import XCTest import RiotSwiftUI @available(iOS 14.0, *) -class SpaceCreationMatrixItemChooserUITests: MockScreenTest { +class MatrixItemChooserUITests: MockScreenTest { override class var screenType: MockScreenState.Type { - return MockSpaceCreationMatrixItemChooserScreenState.self + return MockMatrixItemChooserScreenState.self } override class func createTest() -> MockScreenTest { - return SpaceCreationMatrixItemChooserUITests(selector: #selector(verifySpaceCreationMatrixItemChooserScreen)) + return MatrixItemChooserUITests(selector: #selector(verifyMatrixItemChooserScreen)) } - func verifySpaceCreationMatrixItemChooserScreen() throws { - guard let screenState = screenState as? MockSpaceCreationMatrixItemChooserScreenState else { fatalError("no screen") } + func verifyMatrixItemChooserScreen() throws { + guard let screenState = screenState as? MockMatrixItemChooserScreenState else { fatalError("no screen") } switch screenState { case .noItems: verifyEmptyScreen() diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/Unit/SpaceCreationMatrixItemChooserViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift similarity index 60% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/Unit/SpaceCreationMatrixItemChooserViewModelTests.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift index 4838335798..03750e4bfa 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Test/Unit/SpaceCreationMatrixItemChooserViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser // // Copyright 2021 New Vector Ltd // @@ -22,25 +20,23 @@ import Combine @testable import RiotSwiftUI @available(iOS 14.0, *) -class SpaceCreationMatrixItemChooserViewModelTests: XCTestCase { - +class MatrixItemChooserViewModelTests: XCTestCase { var creationParameters = SpaceCreationParameters() - var service: MockSpaceCreationMatrixItemChooserService! - var viewModel: SpaceCreationMatrixItemChooserViewModelProtocol! - var context: SpaceCreationMatrixItemChooserViewModel.Context! + var service: MockMatrixItemChooserService! + var viewModel: MatrixItemChooserViewModelProtocol! + var context: MatrixItemChooserViewModel.Context! override func setUpWithError() throws { - service = MockSpaceCreationMatrixItemChooserService(type: .room) - viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: creationParameters) + service = MockMatrixItemChooserService(type: .room) + viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: VectorL10n.spacesCreationAddRoomsTitle, detail: VectorL10n.spacesCreationAddRoomsMessage) context = viewModel.context } func testInitialState() { - XCTAssertEqual(context.viewState.navTitle, creationParameters.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle) XCTAssertEqual(context.viewState.emptyListMessage, VectorL10n.spacesNoResultFoundTitle) XCTAssertEqual(context.viewState.title, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(context.viewState.message, VectorL10n.spacesCreationAddRoomsMessage) - XCTAssertEqual(context.viewState.items, MockSpaceCreationMatrixItemChooserService.mockItems) + XCTAssertEqual(context.viewState.items, MockMatrixItemChooserService.mockItems) XCTAssertEqual(context.viewState.selectedItemIds.count, 0) } @@ -48,6 +44,6 @@ class SpaceCreationMatrixItemChooserViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.selectedItemIds.count, 0) service.simulateSelectionForItem(at: 0) XCTAssertEqual(context.viewState.selectedItemIds.count, 1) - XCTAssertEqual(context.viewState.selectedItemIds.first, MockSpaceCreationMatrixItemChooserService.mockItems[0].id) + XCTAssertEqual(context.viewState.selectedItemIds.first, MockMatrixItemChooserService.mockItems[0].id) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift new file mode 100644 index 0000000000..51a73eaa96 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift @@ -0,0 +1,125 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser +// +// 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 MatrixItemChooser: View { + + // MARK: Properties + + @ObservedObject var viewModel: MatrixItemChooserViewModel.Context + @State var searchText: String = "" + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + private var spacerHeight: CGFloat { + if viewModel.viewState.title != nil || viewModel.viewState.message != nil { + return 24 + } else { + return 8 + } + } + + // MARK: Public + + var body: some View { + listContent + .background(Color.clear) + .modifier(WaitOverlay(isLoading: .constant(viewModel.viewState.loading))) + .alert(isPresented: .constant(viewModel.viewState.error != nil)) { + Alert(title: Text(VectorL10n.error), message: Text(viewModel.viewState.error ?? ""), dismissButton: .cancel(Text(VectorL10n.ok))) + } + } + + // MARK: Private + + @ViewBuilder + private var listContent: some View { + ScrollView { + headerView + if viewModel.viewState.items.isEmpty { + Text(viewModel.viewState.emptyListMessage) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .accessibility(identifier: "emptyListMessage") + Spacer() + } else { + LazyVStack(spacing: 0) { + ForEach(viewModel.viewState.items) { item in + MatrixItemChooserListRow( + avatar: item.avatar, + displayName: item.displayName, + detailText: item.detailText, + isSelected: viewModel.viewState.selectedItemIds.contains(item.id) + ) + .onTapGesture { + viewModel.send(viewAction: .itemTapped(item.id)) + } + } + } + .accessibility(identifier: "itemsList") + .frame(maxHeight: .infinity, alignment: .top) + .animation(nil) + } + } + } + + @ViewBuilder + private var headerView: some View { + VStack { + if let title = viewModel.viewState.title { + Text(title) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + .padding(.horizontal) + .padding(.vertical, 8) + .accessibility(identifier: "titleText") + } + if let message = viewModel.viewState.message { + Text(message) + .font(theme.fonts.callout) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + .padding(.horizontal) + .accessibility(identifier: "messageText") + } + Spacer().frame(height: spacerHeight) + SearchBar(placeholder: VectorL10n.searchDefaultPlaceholder, text: $searchText) + .onChange(of: searchText) { value in + viewModel.send(viewAction: .searchTextChanged(searchText)) + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct MatrixItemChooser_Previews: PreviewProvider { + + static let stateRenderer = MockMatrixItemChooserScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup(addNavigation: true) + .theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup(addNavigation: true) + .theme(.dark).preferredColorScheme(.dark) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooserListRow.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift similarity index 94% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooserListRow.swift rename to RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift index 0144fce205..bbc196524e 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooserListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ import SwiftUI @available(iOS 14.0, *) -struct SpaceCreationMatrixItemChooserListRow: View { +struct MatrixItemChooserListRow: View { // MARK: - Properties @@ -65,7 +65,7 @@ struct SpaceCreationMatrixItemChooserListRow: View { // MARK: - Previews @available(iOS 14.0, *) -struct SpaceCreationMatrixItemChooserListRow_Previews: PreviewProvider { +struct MatrixItemChooserListRow_Previews: PreviewProvider { static var previews: some View { TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice") .addDependency(MockAvatarService.example) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift index c5fa17cca2..543a990d0a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift @@ -204,11 +204,18 @@ final class SpaceCreationCoordinator: Coordinator { } return coordinator } - + @available(iOS 14.0, *) - private func createPeopleChooserCoordinator() -> SpaceCreationMatrixItemChooserCoordinator { - let coordinator = SpaceCreationMatrixItemChooserCoordinator(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters(session: parameters.session, type: .people, creationParams: parameters.creationParameters)) - coordinator.callback = { [weak self] result in + private func createPeopleChooserCoordinator() -> MatrixItemChooserCoordinator { + let parameters = MatrixItemChooserCoordinatorParameters( + session: parameters.session, + title: VectorL10n.spacesCreationInviteByUsernameTitle, + detail: VectorL10n.spacesCreationInviteByUsernameMessage, + selectedItemsIds: parameters.creationParameters.userIdInvites, + viewProvider: SpaceCreationMatrixItemChooserViewProvider(), + itemsProcessor: SpaceCreationInviteUsersItemsProcessor(creationParams: parameters.creationParameters)) + let coordinator = MatrixItemChooserCoordinator(parameters: parameters) + coordinator.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: @@ -223,9 +230,16 @@ final class SpaceCreationCoordinator: Coordinator { } @available(iOS 14.0, *) - private func createRoomChooserCoordinator() -> SpaceCreationMatrixItemChooserCoordinator { - let coordinator = SpaceCreationMatrixItemChooserCoordinator(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters(session: parameters.session, type: .room, creationParams: parameters.creationParameters)) - coordinator.callback = { [weak self] result in + private func createRoomChooserCoordinator() -> MatrixItemChooserCoordinator { + let parameters = MatrixItemChooserCoordinatorParameters( + session: parameters.session, + title: VectorL10n.spacesCreationAddRoomsTitle, + detail: VectorL10n.spacesCreationAddRoomsMessage, + selectedItemsIds: parameters.creationParameters.addedRoomIds ?? [], + viewProvider: SpaceCreationMatrixItemChooserViewProvider(), + itemsProcessor: SpaceCreationAddRoomsItemsProcessor(creationParams: parameters.creationParameters)) + let coordinator = MatrixItemChooserCoordinator(parameters: parameters) + coordinator.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinator.swift deleted file mode 100644 index c3399db0cf..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserCoordinator.swift +++ /dev/null @@ -1,74 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser -/* - 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 UIKit -import SwiftUI - -final class SpaceCreationMatrixItemChooserCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - - private let parameters: SpaceCreationMatrixItemChooserCoordinatorParameters - private let spaceCreationMatrixItemChooserHostingController: UIViewController - private var spaceCreationMatrixItemChooserViewModel: SpaceCreationMatrixItemChooserViewModelProtocol - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var callback: ((SpaceCreationMatrixItemChooserCoordinatorAction) -> Void)? - - // MARK: - Setup - - @available(iOS 14.0, *) - init(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters) { - self.parameters = parameters - let service = SpaceCreationMatrixItemChooserService(session: parameters.session, type: parameters.type, selectedItemIds: []) - let viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: parameters.creationParams) - let view = SpaceCreationMatrixItemChooser(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) - spaceCreationMatrixItemChooserViewModel = viewModel - let hostingController = VectorHostingController(rootView: view) - hostingController.isNavigationBarHidden = true - spaceCreationMatrixItemChooserHostingController = hostingController - } - - // MARK: - Public - func start() { - MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] did start.") - spaceCreationMatrixItemChooserViewModel.callback = { [weak self] result in - MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] SpaceCreationMatrixItemChooserViewModel did complete with result: \(result).") - guard let self = self else { return } - switch result { - case .cancel: - self.callback?(.cancel) - case .back: - self.callback?(.back) - case .done: - self.callback?(.done) - } - } - } - - func toPresentable() -> UIViewController { - return self.spaceCreationMatrixItemChooserHostingController - } -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift similarity index 66% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewAction.swift rename to RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift index 3a8382c159..e81c4fe468 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,11 @@ // limitations under the License. // -import Foundation +import SwiftUI -/// Actions send from the `View` to the `ViewModel`. -enum SpaceCreationMatrixItemListStateActionListViewAction { - case searchTextChanged(String) - case itemTapped(_ itemId: String) - case done - case cancel - case back +class SpaceCreationMatrixItemChooserViewProvider: MatrixItemChooserCoordinatorViewProvider { + @available(iOS 14, *) + func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { + return AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel)) + } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItem.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItem.swift deleted file mode 100644 index 6562518d59..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItem.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// 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 - -struct SpaceCreationMatrixItem { - let id: String - let avatar: AvatarInput - let displayName: String? - let detailText: String? -} - -extension SpaceCreationMatrixItem: Identifiable, Equatable {} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemChooserCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemChooserCoordinatorAction.swift deleted file mode 100644 index c431b000cf..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemChooserCoordinatorAction.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 - -/// Actions returned by the coordinator callback -enum SpaceCreationMatrixItemChooserCoordinatorAction { - case done - case cancel - case back -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewModelAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewModelAction.swift deleted file mode 100644 index 490d7dc522..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewModelAction.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 - -/// Actions sent by the`ViewModel` to the `Coordinator`. -enum SpaceCreationMatrixItemListStateActionListViewModelAction { - case done - case cancel - case back -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewState.swift deleted file mode 100644 index adf7e24cfa..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemListStateActionListViewState.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// 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 - -/// State managed by the `ViewModel` delivered to the `View`. -struct SpaceCreationMatrixItemListStateActionListViewState: BindableState { - var navTitle: String - var title: String - var message: String - var emptyListMessage: String - var items: [SpaceCreationMatrixItem] - var selectedItemIds: Set -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemType.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemType.swift deleted file mode 100644 index 63b7068e2c..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Model/SpaceCreationMatrixItemType.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 - -enum SpaceCreationMatrixItemType { - case room - case people -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift new file mode 100644 index 0000000000..146e956a14 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift @@ -0,0 +1,46 @@ +// +// 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 SpaceCreationAddRoomsItemsProcessor: MatrixItemChooserProcessorProtocol { + + // MARK: Private + + private let creationParams: SpaceCreationParameters + + // MARK: Setup + + init(creationParams: SpaceCreationParameters) { + self.creationParams = creationParams + } + + // MARK: MatrixItemChooserSelectionProcessorProtocol + + var dataType: MatrixItemChooserType { + .room + } + + func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) { + creationParams.addedRoomIds = itemsIds + completion(.success(())) + } + + func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { + return true + } + +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift new file mode 100644 index 0000000000..f341034ebc --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift @@ -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 + +class SpaceCreationInviteUsersItemsProcessor: MatrixItemChooserProcessorProtocol { + + // MARK: Private + + private let creationParams: SpaceCreationParameters + + // MARK: Setup + + init(creationParams: SpaceCreationParameters) { + self.creationParams = creationParams + } + + // MARK: MatrixItemChooserSelectionProcessorProtocol + + var dataType: MatrixItemChooserType { + .people + } + + func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) { + creationParams.inviteType = .userId + creationParams.userIdInvites = itemsIds + completion(.success(())) + } + + func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { + return true + } + +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift index b4a4d7d98e..a91156d3ed 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift @@ -1,6 +1,4 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,17 +19,16 @@ import SwiftUI @available(iOS 14.0, *) struct SpaceCreationMatrixItemChooser: View { - // MARK: - Properties + // MARK: Properties - @ObservedObject var viewModel: SpaceCreationMatrixItemChooserViewModel.Context - @State var searchText: String = "" + @ObservedObject var viewModel: MatrixItemChooserViewModel.Context // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - + // MARK: Public - + @ViewBuilder var body: some View { VStack { @@ -46,67 +43,16 @@ struct SpaceCreationMatrixItemChooser: View { .navigationBarHidden(true) } + // MARK: Private + @ViewBuilder private var mainView: some View { ZStack(alignment: .bottom) { - listContent + MatrixItemChooser(viewModel: viewModel) footerView } } - @ViewBuilder - private var headerView: some View { - VStack { - Text(viewModel.viewState.title) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) - .padding(.horizontal) - .padding(.vertical, 8) - .accessibility(identifier: "titleText") - Text(viewModel.viewState.message) - .font(theme.fonts.callout) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.center) - .padding(.horizontal) - .accessibility(identifier: "messageText") - Spacer().frame(height: 24) - SearchBar(placeholder: VectorL10n.searchDefaultPlaceholder, text: $searchText) - .onChange(of: searchText, perform: { value in - viewModel.send(viewAction: .searchTextChanged(searchText)) - }) - } - } - - @ViewBuilder - private var listContent: some View { - ScrollView{ - headerView - if viewModel.viewState.items.isEmpty { - Text(viewModel.viewState.emptyListMessage) - .font(theme.fonts.body) - .foregroundColor(theme.colors.secondaryContent) - .accessibility(identifier: "emptyListMessage") - Spacer() - } else { - LazyVStack(spacing: 0) { - ForEach(viewModel.viewState.items) { item in - SpaceCreationMatrixItemChooserListRow( - avatar: item.avatar, - displayName: item.displayName, - detailText: item.detailText, - isSelected: viewModel.viewState.selectedItemIds.contains(item.id) - ) - .onTapGesture { - viewModel.send(viewAction: .itemTapped(item.id)) - } - } - } - .padding(.bottom, 76) - .accessibility(identifier: "itemsList") - .frame(maxHeight: .infinity, alignment: .top) - } - } - } @ViewBuilder private var footerView: some View { @@ -118,17 +64,3 @@ struct SpaceCreationMatrixItemChooser: View { .padding(.bottom) } } - -// MARK: - Previews - -@available(iOS 14.0, *) -struct SpaceCreationMatrixItemChooser_Previews: PreviewProvider { - - static let stateRenderer = MockSpaceCreationMatrixItemChooserScreenState.stateRenderer - static var previews: some View { - stateRenderer.screenGroup(addNavigation: true) - .theme(.light).preferredColorScheme(.light) - stateRenderer.screenGroup(addNavigation: true) - .theme(.dark).preferredColorScheme(.dark) - } -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModel.swift deleted file mode 100644 index 549bd8f6ef..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModel.swift +++ /dev/null @@ -1,121 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser -// -// 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 SpaceCreationMatrixItemChooserViewModelType = StateStoreViewModel -@available(iOS 14, *) -class SpaceCreationMatrixItemChooserViewModel: SpaceCreationMatrixItemChooserViewModelType, SpaceCreationMatrixItemChooserViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - - private var spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol - private var creationParams: SpaceCreationParameters - - // MARK: Public - - var callback: ((SpaceCreationMatrixItemListStateActionListViewModelAction) -> Void)? - - // MARK: - Setup - - static func makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemChooserViewModelProtocol { - return SpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: spaceCreationMatrixItemChooserService, creationParams: creationParams) - } - - private init(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) { - self.spaceCreationMatrixItemChooserService = spaceCreationMatrixItemChooserService - self.creationParams = creationParams - super.init(initialViewState: Self.defaultState(spaceCreationMatrixItemChooserService: spaceCreationMatrixItemChooserService, creationParams: creationParams)) - startObservingItems() - } - - private static func defaultState(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemListStateActionListViewState { - let navTitle = creationParams.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle - let title = spaceCreationMatrixItemChooserService.type == .people ? VectorL10n.spacesCreationInviteByUsernameTitle : VectorL10n.spacesCreationAddRoomsTitle - let message = spaceCreationMatrixItemChooserService.type == .people ? VectorL10n.spacesCreationInviteByUsernameMessage : VectorL10n.spacesCreationAddRoomsMessage - let emptyListMessage = VectorL10n.spacesNoResultFoundTitle - - return SpaceCreationMatrixItemListStateActionListViewState(navTitle: navTitle, title: title, message: message, emptyListMessage: emptyListMessage, items: spaceCreationMatrixItemChooserService.itemsSubject.value, selectedItemIds: spaceCreationMatrixItemChooserService.selectedItemIdsSubject.value) - } - - private func startObservingItems() { - let itemsUpdatePublisher = spaceCreationMatrixItemChooserService.itemsSubject - .map(SpaceCreationMatrixItemListStateAction.updateItems) - .eraseToAnyPublisher() - dispatch(actionPublisher: itemsUpdatePublisher) - - let selectionPublisher = spaceCreationMatrixItemChooserService.selectedItemIdsSubject - .map(SpaceCreationMatrixItemListStateAction.updateSelection) - .eraseToAnyPublisher() - dispatch(actionPublisher: selectionPublisher) - } - - // MARK: - Public - - override func process(viewAction: SpaceCreationMatrixItemListStateActionListViewAction) { - switch viewAction { - case .cancel: - cancel() - case .back: - back() - case .done: - let selectedItemIds = Array(spaceCreationMatrixItemChooserService.selectedItemIdsSubject.value) - switch spaceCreationMatrixItemChooserService.type { - case .people: - creationParams.inviteType = .userId - creationParams.userIdInvites = selectedItemIds - default: - creationParams.addedRoomIds = selectedItemIds - } - done() - case .searchTextChanged(let searchText): - self.spaceCreationMatrixItemChooserService.searchText = searchText - case .itemTapped(let itemId): - self.spaceCreationMatrixItemChooserService.reverseSelectionForItem(withId: itemId) - } - } - - override class func reducer(state: inout SpaceCreationMatrixItemListStateActionListViewState, action: SpaceCreationMatrixItemListStateAction) { - switch action { - case .updateItems(let items): - state.items = items - case .updateSelection(let selectedItemIds): - state.selectedItemIds = selectedItemIds - } - UILog.debug("[SpaceCreationMatrixItemChooserViewModel] reducer with action \(action) produced state: \(state)") - } - - private func done() { - callback?(.done) - } - - private func cancel() { - callback?(.cancel) - } - - private func back() { - callback?(.back) - } -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModelProtocol.swift deleted file mode 100644 index 3009daf328..0000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/ViewModel/SpaceCreationMatrixItemChooserViewModelProtocol.swift +++ /dev/null @@ -1,28 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser -// -// 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 SpaceCreationMatrixItemChooserViewModelProtocol { - - var callback: ((SpaceCreationMatrixItemListStateActionListViewModelAction) -> Void)? { get set } - @available(iOS 14, *) - static func makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: SpaceCreationMatrixItemChooserServiceProtocol, creationParams: SpaceCreationParameters) -> SpaceCreationMatrixItemChooserViewModelProtocol - @available(iOS 14, *) - var context: SpaceCreationMatrixItemChooserViewModelType.Context { get } -} diff --git a/changelog.d/5230.feature b/changelog.d/5230.feature new file mode 100644 index 0000000000..825e377f72 --- /dev/null +++ b/changelog.d/5230.feature @@ -0,0 +1 @@ +Adding Rooms to Spaces \ No newline at end of file