diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift new file mode 100644 index 000000000..d5c029106 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -0,0 +1,235 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room Room +/* + 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 + +final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: RoomCoordinatorParameters + private let roomViewController: RoomViewController + private let activityIndicatorPresenter: ActivityIndicatorPresenterType + private var selectedEventId: String? + + private var roomDataSourceManager: MXKRoomDataSourceManager { + return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) + } + + /// Indicate true if the Coordinator has started once + private var hasStartedOnce: Bool { + return self.roomViewController.delegate != nil + } + + private var navigationRouter: NavigationRouterType? { + + var finalNavigationRouter: NavigationRouterType? + + if let navigationRouter = self.parameters.navigationRouter { + finalNavigationRouter = navigationRouter + } else if let navigationRouterStore = self.parameters.navigationRouterStore, let currentNavigationController = self.roomViewController.navigationController { + // If no navigationRouter has been provided, try to get the navigation router from the current RoomViewController navigation controller if exists + finalNavigationRouter = navigationRouterStore.getOrCreateNavigationRouter(for: currentNavigationController) + } + + return finalNavigationRouter + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: RoomCoordinatorDelegate? + + var canReleaseRoomDataSource: Bool { + // If the displayed data is not a preview, let the manager release the room data source + // (except if the view controller has the room data source ownership). + return self.parameters.previewData == nil && self.roomViewController.roomDataSource != nil && self.roomViewController.hasRoomDataSourceOwnership == false + } + + // MARK: - Setup + + init(parameters: RoomCoordinatorParameters) { + self.parameters = parameters + self.selectedEventId = parameters.eventId + + self.roomViewController = RoomViewController.instantiate() + self.activityIndicatorPresenter = ActivityIndicatorPresenter() + + super.init() + } + + // MARK: - Public + + func start() { + + self.roomViewController.delegate = self + + // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). + self.roomViewController.presentationController?.delegate = self + + if let eventId = self.selectedEventId { + self.start(with: self.parameters.roomId, and: eventId) + } else { + self.start(with: self.parameters.roomId) + } + + // Add `roomViewController` to the NavigationRouter, only if it has been explicity set as parameter + if let navigationRouter = self.parameters.navigationRouter { + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(self.roomViewController, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(self.roomViewController, popCompletion: nil) + } + } + } + + /// Use this method when the room screen is already shown and you want to go to a specific event. + /// i.e User tap on push notification message for the current displayed room + func start(withEventId eventId: String) { + + self.selectedEventId = eventId + + if self.hasStartedOnce { + self.start(with: self.parameters.roomId, and: eventId) + } else { + self.start() + } + } + + func toPresentable() -> UIViewController { + return self.roomViewController + } + + // MARK: - Private + + private func start(with roomId: String) { + + // Present activity indicator when retrieving roomDataSource for given room ID + self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + + let roomDataSourceManager: MXKRoomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) + + // LIVE: Show the room live timeline managed by MXKRoomDataSourceManager + roomDataSourceManager.roomDataSource(forRoom: roomId, create: true, onComplete: { [weak self] (roomDataSource) in + + guard let self = self else { + return + } + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + if let roomDataSource = roomDataSource { + self.roomViewController.displayRoom(roomDataSource) + } + }) + } + + private func start(with roomId: String, and eventId: String) { + + // Present activity indicator when retrieving roomDataSource for given room ID + self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + + // Open the room on the requested event + RoomDataSource.load(withRoomId: roomId, + initialEventId: eventId, + andMatrixSession: self.parameters.session) { [weak self] (dataSource) in + + guard let self = self else { + return + } + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + guard let roomDataSource = dataSource as? RoomDataSource else { + return + } + + roomDataSource.markTimelineInitialEvent = true + self.roomViewController.displayRoom(roomDataSource) + + // Give the data source ownership to the room view controller. + self.roomViewController.hasRoomDataSourceOwnership = true + } + } +} + +// MARK: - RoomIdentifiable +extension RoomCoordinator: RoomIdentifiable { + + var roomId: String? { + return self.parameters.roomId + } + + var mxSession: MXSession? { + self.parameters.session + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension RoomCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.delegate?.roomCoordinatorDidDismissInteractively(self) + } +} + +// MARK: - RoomViewControllerDelegate +extension RoomCoordinator: RoomViewControllerDelegate { + + func roomViewController(_ roomViewController: RoomViewController, showRoomWithId roomID: String) { + self.delegate?.roomCoordinator(self, didSelectRoomWithId: roomID) + } + + func roomViewController(_ roomViewController: RoomViewController, showMemberDetails roomMember: MXRoomMember) { + // TODO: + } + + func roomViewControllerShowRoomDetails(_ roomViewController: RoomViewController) { + // TODO: + } + + func roomViewControllerDidLeaveRoom(_ roomViewController: RoomViewController) { + self.delegate?.roomCoordinatorDidLeaveRoom(self) + } + + func roomViewControllerPreviewDidTapCancel(_ roomViewController: RoomViewController) { + self.delegate?.roomCoordinatorDidCancelRoomPreview(self) + } + + func roomViewController(_ roomViewController: RoomViewController, startChatWithUserId userId: String, completion: @escaping () -> Void) { + AppDelegate.theDelegate().createDirectChat(withUserId: userId, completion: completion) + } + + func roomViewController(_ roomViewController: RoomViewController, showCompleteSecurityFor session: MXSession) { + AppDelegate.theDelegate().presentCompleteSecurity(for: session) + } + + func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkFragment fragment: String, from universalLinkURL: URL?) -> Bool { + return AppDelegate.theDelegate().handleUniversalLinkFragment(fragment, from: universalLinkURL) + } + + func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkURL universalLinkURL: URL) -> Bool { + return AppDelegate.theDelegate().handleUniversalLinkURL(universalLinkURL) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..910014d05 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -0,0 +1,148 @@ +// +// 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 + +@objc protocol RoomCoordinatorBridgePresenterDelegate { + func roomCoordinatorBridgePresenterDidLeaveRoom(_ bridgePresenter: RoomCoordinatorBridgePresenter) + func roomCoordinatorBridgePresenterDidCancelRoomPreview(_ bridgePresenter: RoomCoordinatorBridgePresenter) + func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter, didSelectRoomWithId roomId: String) + func roomCoordinatorBridgePresenterDidDismissInteractively(_ bridgePresenter: RoomCoordinatorBridgePresenter) +} + +@objcMembers +class RoomCoordinatorBridgePresenterParameters: NSObject { + + /// The matrix session in which the room should be available. + let session: MXSession + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The data for the room preview. + let previewData: RoomPreviewData? + + init(session: MXSession, + roomId: String, + eventId: String?, + previewData: RoomPreviewData?) { + self.session = session + self.roomId = roomId + self.eventId = eventId + self.previewData = previewData + } +} + +/// RoomCoordinatorBridgePresenter enables to start RoomCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class RoomCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let bridgeParameters: RoomCoordinatorBridgePresenterParameters + private var coordinator: RoomCoordinator? + + // MARK: Public + + weak var delegate: RoomCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(parameters: RoomCoordinatorBridgePresenterParameters) { + self.bridgeParameters = parameters + super.init() + } + + // MARK: - Public + + func present(from viewController: UIViewController, animated: Bool) { + + let coordinator = self.createRoomCoordinator() + coordinator.delegate = self + let presentable = coordinator.toPresentable() + presentable.modalPresentationStyle = .formSheet + viewController.present(presentable, animated: animated, completion: nil) + coordinator.start() + + self.coordinator = coordinator + } + + func push(from navigationController: UINavigationController, animated: Bool) { + + let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + + let coordinator = self.createRoomCoordinator(with: navigationRouter) + coordinator.delegate = self + coordinator.start() // Will trigger view controller push + + self.coordinator = coordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } + + // MARK: - Private + + private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) -> RoomCoordinator { + + let coordinatorParameters: RoomCoordinatorParameters + + if let previewData = self.bridgeParameters.previewData { + coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, previewData: previewData) + } else { + coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, session: self.bridgeParameters.session, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId) + } + + return RoomCoordinator(parameters: coordinatorParameters) + } +} + +// MARK: - RoomNotificationSettingsCoordinatorDelegate +extension RoomCoordinatorBridgePresenter: RoomCoordinatorDelegate { + + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) { + self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId) + } + + + func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidLeaveRoom(self) + } + + func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidCancelRoomPreview(self) + } + + func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidDismissInteractively(self) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift new file mode 100644 index 000000000..fbc9f3511 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift @@ -0,0 +1,76 @@ +// +// 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 + +/// RoomCoordinator input parameters +struct RoomCoordinatorParameters { + + // MARK: - Properties + + /// The navigation router that manage physical navigation + let navigationRouter: NavigationRouterType? + + /// The navigation router store that enables to get a NavigationRouter from a navigation controller + /// `navigationRouter` property takes priority on `navigationRouterStore` + let navigationRouterStore: NavigationRouterStoreProtocol? + + /// The matrix session in which the room should be available. + let session: MXSession + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The data for the room preview. + let previewData: RoomPreviewData? + + // MARK: - Setup + + private init(navigationRouter: NavigationRouterType?, + navigationRouterStore: NavigationRouterStoreProtocol?, + session: MXSession, + roomId: String, + eventId: String?, + previewData: RoomPreviewData?) { + self.navigationRouter = navigationRouter + self.navigationRouterStore = navigationRouterStore + self.session = session + self.roomId = roomId + self.eventId = eventId + self.previewData = previewData + } + + /// Init to present a joined room + init(navigationRouter: NavigationRouterType? = nil, + navigationRouterStore: NavigationRouterStoreProtocol? = nil, + session: MXSession, + roomId: String, + eventId: String? = nil) { + + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, previewData: nil) + } + + /// Init to present a room preview + init(navigationRouter: NavigationRouterType? = nil, + navigationRouterStore: NavigationRouterStoreProtocol? = nil, + previewData: RoomPreviewData) { + + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, previewData: previewData) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorProtocol.swift b/Riot/Modules/Room/RoomCoordinatorProtocol.swift new file mode 100644 index 000000000..856323167 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorProtocol.swift @@ -0,0 +1,33 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room Room +/* + 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 RoomCoordinatorDelegate: AnyObject { + func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) + func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) + func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) +} + +/// `RoomCoordinatorProtocol` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +protocol RoomCoordinatorProtocol: Coordinator, Presentable, RoomIdentifiable { + var delegate: RoomCoordinatorDelegate? { get } + + var canReleaseRoomDataSource: Bool { get } +} diff --git a/Riot/Modules/Room/RoomIdentifiable.swift b/Riot/Modules/Room/RoomIdentifiable.swift new file mode 100644 index 000000000..2ccac42d7 --- /dev/null +++ b/Riot/Modules/Room/RoomIdentifiable.swift @@ -0,0 +1,24 @@ +// +// 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 + +/// `RoomIdentifiable` describes an object tied to a specific room id. +/// Useful to identify existing objects that should be removed when the user leaves a room for example. +protocol RoomIdentifiable { + var roomId: String? { get } + var mxSession: MXSession? { get } +}