mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
SwiftUI NavigationController and UserNotificationControllers (#309)
* Fixes #286 - Adopted the new SwiftUI NavigationStack based NavigationController throughout the application * Fixes #315 - Implemented new user notification components on top of SwiftUI and the new navigation flows * Add home screen fade animation between skeletons and real rooms * Bump the danger-swift version used on the CI and swiftlint with it * Renamed Splash to Onboarding, Empty to Splash
This commit is contained in:
@@ -21,39 +21,22 @@ struct AnalyticsPromptCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
}
|
||||
|
||||
final class AnalyticsPromptCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
||||
private let parameters: AnalyticsPromptCoordinatorParameters
|
||||
private let analyticsPromptHostingController: UIViewController
|
||||
private var analyticsPromptViewModel: AnalyticsPromptViewModel
|
||||
|
||||
// MARK: Public
|
||||
private var viewModel: AnalyticsPromptViewModel
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor () -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: AnalyticsPromptCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
|
||||
|
||||
let view = AnalyticsPrompt(context: viewModel.context)
|
||||
analyticsPromptViewModel = viewModel
|
||||
analyticsPromptHostingController = UIHostingController(rootView: view)
|
||||
viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
analyticsPromptViewModel.callback = { [weak self] result in
|
||||
viewModel.callback = { [weak self] result in
|
||||
MXLog.debug("AnalyticsPromptViewModel did complete with result: \(result).")
|
||||
|
||||
guard let self else { return }
|
||||
@@ -69,7 +52,7 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController { analyticsPromptHostingController }
|
||||
|
||||
func stop() { }
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(AnalyticsPrompt(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
@@ -22,42 +22,31 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
didLoginWithSession userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let navigationRouter: NavigationRouter
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
private let navigationController: NavigationController
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
navigationRouter: NavigationRouter) {
|
||||
navigationController: NavigationController) {
|
||||
self.authenticationService = authenticationService
|
||||
self.navigationRouter = navigationRouter
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: navigationRouter.toPresentable())
|
||||
self.navigationController = navigationController
|
||||
}
|
||||
|
||||
func start() {
|
||||
showSplashScreen()
|
||||
showOnboarding()
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
navigationRouter.toPresentable()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Shows the splash screen as the root view in the navigation stack.
|
||||
private func showSplashScreen() {
|
||||
let coordinator = SplashScreenCoordinator()
|
||||
|
||||
private func showOnboarding() {
|
||||
let coordinator = OnboardingCoordinator()
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
@@ -66,12 +55,7 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func startAuthentication() async {
|
||||
@@ -89,7 +73,8 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
|
||||
private func showServerSelectionScreen() {
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
hasModalPresentation: false)
|
||||
userNotificationController: ServiceLocator.shared.userNotificationController,
|
||||
isModallyPresented: false)
|
||||
let coordinator = ServerSelectionCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@@ -103,34 +88,24 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginCoordinatorParameters(authenticationService: authenticationService,
|
||||
navigationRouter: navigationRouter)
|
||||
navigationController: navigationController)
|
||||
let coordinator = LoginCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
switch action {
|
||||
case .signedIn(let userSession):
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
||||
@@ -141,22 +116,20 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
guard let self else { return }
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
/// Show a blocking activity indicator.
|
||||
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
|
||||
private func startLoading() {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct LoginCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
/// The navigation router used to present the server selection screen.
|
||||
let navigationRouter: NavigationRouterType
|
||||
let navigationController: NavigationController
|
||||
}
|
||||
|
||||
enum LoginCoordinatorAction {
|
||||
@@ -29,14 +29,10 @@ enum LoginCoordinatorAction {
|
||||
case signedIn(UserSessionProtocol)
|
||||
}
|
||||
|
||||
final class LoginCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class LoginCoordinator: CoordinatorProtocol {
|
||||
private let parameters: LoginCoordinatorParameters
|
||||
private let loginHostingController: UIViewController
|
||||
private var loginViewModel: LoginViewModelProtocol
|
||||
private var viewModel: LoginViewModelProtocol
|
||||
private let hostingController: UIViewController
|
||||
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
|
||||
private let oidcUserAgent: OIDExternalUserAgentIOS?
|
||||
|
||||
@@ -47,14 +43,8 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
private var navigationController: NavigationController { parameters.navigationController }
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (LoginCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
@@ -62,22 +52,16 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
init(parameters: LoginCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
|
||||
loginViewModel = viewModel
|
||||
viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
|
||||
|
||||
let view = LoginScreen(context: viewModel.context)
|
||||
loginHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController)
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: loginHostingController)
|
||||
hostingController = UIHostingController(rootView: LoginScreen(context: viewModel.context))
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
loginViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("LoginViewModel did callback with result: \(action).")
|
||||
|
||||
@@ -95,51 +79,52 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
loginHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(LoginScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show a blocking activity indicator whilst saving.
|
||||
static let loadingIndicatorIdentifier = "LoginCoordinatorLoading"
|
||||
|
||||
private func startLoading(isInteractionBlocking: Bool) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: isInteractionBlocking))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
|
||||
if !isInteractionBlocking {
|
||||
loginViewModel.update(isLoading: true)
|
||||
viewModel.update(isLoading: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a non-blocking indicator that an operation was successful.
|
||||
private func indicateSuccess() {
|
||||
activityIndicator = indicatorPresenter.present(.success(label: ElementL10n.dialogTitleSuccess))
|
||||
}
|
||||
|
||||
/// Show a non-blocking indicator that an operation failed.
|
||||
private func indicateFailure() {
|
||||
activityIndicator = indicatorPresenter.present(.error(label: ElementL10n.dialogTitleError))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loginViewModel.update(isLoading: false)
|
||||
activityIndicator = nil
|
||||
viewModel.update(isLoading: false)
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
private func indicateSuccess() {
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleSuccess))
|
||||
}
|
||||
|
||||
private func indicateFailure() {
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleError))
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
loginViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
viewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,44 +189,43 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
|
||||
/// Updates the view model with a different homeserver.
|
||||
private func updateViewModel() {
|
||||
loginViewModel.update(homeserver: authenticationService.homeserver)
|
||||
viewModel.update(homeserver: authenticationService.homeserver)
|
||||
indicateSuccess()
|
||||
}
|
||||
|
||||
/// Presents the server selection screen as a modal.
|
||||
private func presentServerSelectionScreen() {
|
||||
MXLog.debug("PresentServerSelectionScreen")
|
||||
let serverSelectionNavigationController = NavigationController()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationController)
|
||||
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
hasModalPresentation: true)
|
||||
userNotificationController: userNotificationController,
|
||||
isModallyPresented: true)
|
||||
|
||||
let coordinator = ServerSelectionCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] action in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
serverSelectionNavigationController.setRootCoordinator(coordinator)
|
||||
|
||||
let modalRouter = NavigationRouter(navigationController: ElementNavigationController())
|
||||
modalRouter.setRootModule(coordinator)
|
||||
|
||||
navigationRouter.present(modalRouter, animated: true)
|
||||
navigationController.presentSheet(userNotificationController)
|
||||
}
|
||||
|
||||
/// Handles the result from the server selection modal, dismissing it after updating the view.
|
||||
private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator,
|
||||
didCompleteWith action: ServerSelectionCoordinatorAction) {
|
||||
navigationRouter.dismissModule(animated: true) { [weak self] in
|
||||
if action == .updated {
|
||||
self?.updateViewModel()
|
||||
}
|
||||
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
if action == .updated {
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
navigationController.dismissSheet()
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
private func showForgotPasswordScreen() {
|
||||
loginViewModel.displayError(.alert("Not implemented."))
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -27,18 +27,18 @@ enum MockServerSelectionScreenState: CaseIterable {
|
||||
switch self {
|
||||
case .matrix:
|
||||
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
case .emptyAddress:
|
||||
return ServerSelectionViewModel(homeserverAddress: "",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
case .invalidAddress:
|
||||
let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad",
|
||||
hasModalPresentation: true)
|
||||
isModallyPresented: true)
|
||||
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
return viewModel
|
||||
case .nonModal:
|
||||
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
|
||||
hasModalPresentation: false)
|
||||
isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-44
@@ -19,8 +19,9 @@ import SwiftUI
|
||||
struct ServerSelectionCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
let hasModalPresentation: Bool
|
||||
let isModallyPresented: Bool
|
||||
}
|
||||
|
||||
enum ServerSelectionCoordinatorAction {
|
||||
@@ -28,45 +29,25 @@ enum ServerSelectionCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class ServerSelectionCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ServerSelectionCoordinatorParameters
|
||||
private let serverSelectionHostingController: UIViewController
|
||||
private var serverSelectionViewModel: ServerSelectionViewModelProtocol
|
||||
|
||||
private let userNotificationController: UserNotificationControllerProtocol
|
||||
private var viewModel: ServerSelectionViewModelProtocol
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: ServerSelectionCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
|
||||
hasModalPresentation: parameters.hasModalPresentation)
|
||||
let view = ServerSelectionScreen(context: viewModel.context)
|
||||
serverSelectionViewModel = viewModel
|
||||
serverSelectionHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController)
|
||||
viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
userNotificationController = parameters.userNotificationController
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
serverSelectionViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("ServerSelectionViewModel did callback with action: \(action).")
|
||||
|
||||
@@ -79,27 +60,24 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
serverSelectionHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ServerSelectionScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func startLoading(label: String = ElementL10n.loading) {
|
||||
userNotificationController.submitNotification(UserNotification(type: .modal,
|
||||
title: label,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
userNotificationController.retractAllNotifications()
|
||||
}
|
||||
|
||||
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
|
||||
@@ -122,9 +100,9 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidServer, .invalidHomeserverAddress:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
|
||||
viewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
|
||||
default:
|
||||
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ struct ServerSelectionViewState: BindableState {
|
||||
/// An error message to be shown in the text field footer.
|
||||
var footerErrorMessage: String?
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
var hasModalPresentation: Bool
|
||||
var isModallyPresented: Bool
|
||||
|
||||
/// The message to show in the text field footer.
|
||||
var footerMessage: String {
|
||||
@@ -42,7 +42,7 @@ struct ServerSelectionViewState: BindableState {
|
||||
|
||||
/// The title shown on the confirm button.
|
||||
var buttonTitle: String {
|
||||
hasModalPresentation ? ElementL10n.actionConfirm : ElementL10n.actionNext
|
||||
isModallyPresented ? ElementL10n.actionConfirm : ElementL10n.actionNext
|
||||
}
|
||||
|
||||
/// The text field is showing an error.
|
||||
|
||||
+4
-4
@@ -28,13 +28,13 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
|
||||
var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(homeserverAddress: String, hasModalPresentation: Bool) {
|
||||
|
||||
init(homeserverAddress: String, isModallyPresented: Bool) {
|
||||
let bindings = ServerSelectionBindings(homeserverAddress: homeserverAddress)
|
||||
super.init(initialViewState: ServerSelectionViewState(bindings: bindings,
|
||||
hasModalPresentation: hasModalPresentation))
|
||||
isModallyPresented: isModallyPresented))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ServerSelectionViewAction) async {
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@ struct ServerSelectionScreen: View {
|
||||
.background(Color.element.background, ignoresSafeAreaEdges: .all)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
/// The title, message and icon at the top of the screen.
|
||||
@@ -91,7 +92,7 @@ struct ServerSelectionScreen: View {
|
||||
@ToolbarContentBuilder
|
||||
var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if context.viewState.hasModalPresentation {
|
||||
if context.viewState.isModallyPresented {
|
||||
Button { context.send(viewAction: .dismiss) } label: {
|
||||
Text(ElementL10n.actionCancel)
|
||||
}
|
||||
|
||||
@@ -40,55 +40,35 @@ enum SoftLogoutCoordinatorResult: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class SoftLogoutCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SoftLogoutCoordinatorParameters
|
||||
private let softLogoutHostingController: UIViewController
|
||||
private var softLogoutViewModel: SoftLogoutViewModelProtocol
|
||||
private var viewModel: SoftLogoutViewModelProtocol
|
||||
private let hostingController: UIViewController
|
||||
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
|
||||
private let oidcUserAgent: OIDExternalUserAgentIOS?
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var successIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (SoftLogoutCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: SoftLogoutCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let homeserver = parameters.authenticationService.homeserver
|
||||
|
||||
let viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
softLogoutViewModel = viewModel
|
||||
|
||||
let view = SoftLogoutScreen(context: viewModel.context)
|
||||
softLogoutHostingController = UIHostingController(rootView: view)
|
||||
viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: softLogoutHostingController)
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: softLogoutHostingController)
|
||||
hostingController = UIHostingController(rootView: SoftLogoutScreen(context: viewModel.context))
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] did start.")
|
||||
|
||||
softLogoutViewModel.callback = { [weak self] result in
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).")
|
||||
|
||||
@@ -105,31 +85,36 @@ final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
softLogoutHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SoftLogoutScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static let loadingIndicatorIdentifier = "SoftLogoutLoading"
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
@MainActor private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: ElementL10n.loading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
@MainActor private func showForgotPasswordScreen() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] showForgotPasswordScreen")
|
||||
|
||||
softLogoutViewModel.displayError(.alert("Not implemented."))
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
|
||||
/// Login with the supplied username and password.
|
||||
@@ -177,11 +162,11 @@ final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
viewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,90 +16,77 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BugReportCoordinatorParameters {
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let screenshot: UIImage?
|
||||
enum BugReportCoordinatorResult {
|
||||
case cancel
|
||||
case finish
|
||||
}
|
||||
|
||||
final class BugReportCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: BugReportCoordinatorParameters
|
||||
private let bugReportHostingController: UIViewController
|
||||
private var bugReportViewModel: BugReportViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var statusIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
struct BugReportCoordinatorParameters {
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
let screenshot: UIImage?
|
||||
let isModallyPresented: Bool
|
||||
}
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
final class BugReportCoordinator: CoordinatorProtocol {
|
||||
private let parameters: BugReportCoordinatorParameters
|
||||
private var viewModel: BugReportViewModelProtocol
|
||||
|
||||
var completion: ((BugReportCoordinatorResult) -> Void)?
|
||||
|
||||
init(parameters: BugReportCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = BugReportViewModel(bugReportService: parameters.bugReportService,
|
||||
screenshot: parameters.screenshot)
|
||||
let view = BugReportScreen(context: viewModel.context)
|
||||
bugReportViewModel = viewModel
|
||||
bugReportHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: bugReportHostingController)
|
||||
viewModel = BugReportViewModel(bugReportService: parameters.bugReportService,
|
||||
screenshot: parameters.screenshot,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
bugReportViewModel.callback = { [weak self] result in
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("BugReportViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .cancel:
|
||||
self.completion?(.cancel)
|
||||
case .submitStarted:
|
||||
self.startLoading()
|
||||
case .submitFinished:
|
||||
self.stopLoading()
|
||||
self.completion?()
|
||||
self.completion?(.finish)
|
||||
case .submitFailed(let error):
|
||||
self.stopLoading()
|
||||
self.showError(label: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
bugReportHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(BugReportScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label,
|
||||
isInteractionBlocking: isInteractionBlocking))
|
||||
static let loadingIndicatorIdentifier = "BugReportLoading"
|
||||
|
||||
private func startLoading(label: String = ElementL10n.loading) {
|
||||
parameters.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: label,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
parameters.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
/// Show error indicator
|
||||
|
||||
private func showError(label: String) {
|
||||
statusIndicator = indicatorPresenter.present(.error(label: label))
|
||||
parameters.userNotificationController.submitNotification(UserNotification(title: label))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import UIKit
|
||||
// MARK: View model
|
||||
|
||||
enum BugReportViewModelAction {
|
||||
case cancel
|
||||
case submitStarted
|
||||
case submitFinished
|
||||
case submitFailed(error: Error)
|
||||
@@ -32,6 +33,7 @@ enum BugReportViewModelAction {
|
||||
struct BugReportViewState: BindableState {
|
||||
var screenshot: UIImage?
|
||||
var bindings: BugReportViewStateBindings
|
||||
let isModallyPresented: Bool
|
||||
}
|
||||
|
||||
struct BugReportViewStateBindings {
|
||||
@@ -40,6 +42,7 @@ struct BugReportViewStateBindings {
|
||||
}
|
||||
|
||||
enum BugReportViewAction {
|
||||
case cancel
|
||||
case submit
|
||||
case toggleSendLogs
|
||||
case removeScreenshot
|
||||
|
||||
@@ -16,18 +16,41 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias BugReportViewModelType = StateStoreViewModel<BugReportViewState,
|
||||
BugReportViewAction>
|
||||
@available(iOS 14, *)
|
||||
class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
typealias BugReportViewModelType = StateStoreViewModel<BugReportViewState, BugReportViewAction>
|
||||
|
||||
class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
|
||||
var callback: ((BugReportViewModelAction) -> Void)?
|
||||
|
||||
init(bugReportService: BugReportServiceProtocol,
|
||||
screenshot: UIImage?,
|
||||
isModallyPresented: Bool) {
|
||||
self.bugReportService = bugReportService
|
||||
let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true)
|
||||
super.init(initialViewState: BugReportViewState(screenshot: screenshot,
|
||||
bindings: bindings,
|
||||
isModallyPresented: isModallyPresented))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: BugReportViewAction) async {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
case .submit:
|
||||
await submitBugReport()
|
||||
case .toggleSendLogs:
|
||||
context.sendingLogsEnabled.toggle()
|
||||
case .removeScreenshot:
|
||||
state.screenshot = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
func submitBugReport() async {
|
||||
private func submitBugReport() async {
|
||||
callback?(.submitStarted)
|
||||
do {
|
||||
var files: [URL] = []
|
||||
@@ -54,31 +77,4 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
|
||||
callback?(.submitFailed(error: error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((BugReportViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(bugReportService: BugReportServiceProtocol,
|
||||
screenshot: UIImage?) {
|
||||
self.bugReportService = bugReportService
|
||||
let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true)
|
||||
super.init(initialViewState: BugReportViewState(screenshot: screenshot,
|
||||
bindings: bindings))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: BugReportViewAction) async {
|
||||
switch viewAction {
|
||||
case .submit:
|
||||
await submitBugReport()
|
||||
case .toggleSendLogs:
|
||||
context.sendingLogsEnabled.toggle()
|
||||
case .removeScreenshot:
|
||||
state.screenshot = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,5 @@ import Foundation
|
||||
@MainActor
|
||||
protocol BugReportViewModelProtocol {
|
||||
var callback: ((BugReportViewModelAction) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
var context: BugReportViewModelType.Context { get }
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@ struct BugReportScreen: View {
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
}
|
||||
.navigationTitle(ElementL10n.titleActivityBugReport)
|
||||
.toolbar {
|
||||
if context.viewState.isModallyPresented {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(ElementL10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +146,7 @@ struct BugReport_Previews: PreviewProvider {
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image)
|
||||
let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image, isModallyPresented: false)
|
||||
BugReportScreen(context: viewModel.context)
|
||||
.previewInterfaceOrientation(.portrait)
|
||||
}
|
||||
|
||||
@@ -25,42 +25,22 @@ enum FilePreviewCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class FilePreviewCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class FilePreviewCoordinator: CoordinatorProtocol {
|
||||
private let parameters: FilePreviewCoordinatorParameters
|
||||
private let filePreviewHostingController: UIViewController
|
||||
private var filePreviewViewModel: FilePreviewViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
private var viewModel: FilePreviewViewModelProtocol
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((FilePreviewCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: FilePreviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = FilePreviewViewModel(fileURL: parameters.fileURL, title: parameters.title)
|
||||
let view = FilePreviewScreen(context: viewModel.context)
|
||||
filePreviewViewModel = viewModel
|
||||
filePreviewHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: filePreviewHostingController)
|
||||
viewModel = FilePreviewViewModel(fileURL: parameters.fileURL, title: parameters.title)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
filePreviewViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("FilePreviewViewModel did complete with result: \(action).")
|
||||
switch action {
|
||||
@@ -70,26 +50,7 @@ final class FilePreviewCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
filePreviewHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(FilePreviewScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import SwiftUI
|
||||
struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let navigationController: NavigationController
|
||||
}
|
||||
|
||||
enum HomeScreenCoordinatorAction {
|
||||
@@ -30,34 +32,20 @@ enum HomeScreenCoordinatorAction {
|
||||
case signOut
|
||||
}
|
||||
|
||||
final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class HomeScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: HomeScreenCoordinatorParameters
|
||||
private let hostingController: UIViewController
|
||||
private var viewModel: HomeScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((HomeScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: HomeScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
|
||||
attributedStringBuilder: parameters.attributedStringBuilder)
|
||||
|
||||
let view = HomeScreen(context: viewModel.context)
|
||||
hostingController = UIHostingController(rootView: view)
|
||||
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -74,13 +62,21 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() { }
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
hostingController
|
||||
func start() {
|
||||
if parameters.bugReportService.crashedLastRun {
|
||||
viewModel.presentAlert(
|
||||
AlertInfo(id: UUID(),
|
||||
title: ElementL10n.sendBugReportAppCrashed,
|
||||
primaryButton: .init(title: ElementL10n.no, action: nil),
|
||||
secondaryButton: .init(title: ElementL10n.yes) { [weak self] in
|
||||
self?.callback?(.presentFeedbackScreen)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(HomeScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -98,11 +94,6 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
private func presentInviteFriends() {
|
||||
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: parameters.userSession.userID).absoluteString else {
|
||||
return
|
||||
}
|
||||
let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink)
|
||||
let vc = UIActivityViewController(activityItems: [shareText], applicationActivities: nil)
|
||||
hostingController.present(vc, animated: true)
|
||||
parameters.navigationController.presentSheet(InviteFriendsCoordinator(userId: parameters.userSession.userID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ struct HomeScreenViewState: BindableState {
|
||||
|
||||
var rooms: [HomeScreenRoom] = []
|
||||
|
||||
var roomListMode: HomeScreenRoomListMode = .rooms
|
||||
var roomListMode: HomeScreenRoomListMode = .skeletons
|
||||
|
||||
var visibleRooms: [HomeScreenRoom] {
|
||||
if roomListMode == .skeletons {
|
||||
@@ -77,6 +77,8 @@ struct HomeScreenViewState: BindableState {
|
||||
|
||||
struct HomeScreenViewStateBindings {
|
||||
var searchQuery = ""
|
||||
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
}
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
@@ -92,15 +94,12 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
var isPlaceholder = false
|
||||
|
||||
static func placeholder(id: String) -> HomeScreenRoom {
|
||||
HomeScreenRoom(id: id,
|
||||
name: "Placeholder room name",
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
lastMessage: AttributedString("Last message"),
|
||||
avatar: UIImage(systemName: "photo"),
|
||||
isPlaceholder: true)
|
||||
avatar: UIImage(systemName: "photo"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func presentAlert(_ alertInfo: AlertInfo<UUID>) {
|
||||
state.bindings.alertInfo = alertInfo
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
|
||||
@@ -22,4 +22,6 @@ protocol HomeScreenViewModelProtocol {
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)? { get set }
|
||||
|
||||
var context: HomeScreenViewModelType.Context { get }
|
||||
|
||||
func presentAlert(_ alert: AlertInfo<UUID>)
|
||||
}
|
||||
|
||||
@@ -28,23 +28,30 @@ struct HomeScreen: View {
|
||||
sessionVerificationBanner
|
||||
}
|
||||
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
if room.isPlaceholder {
|
||||
if context.viewState.roomListMode == .skeletons {
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
.redacted(reason: .placeholder)
|
||||
.disabled(true)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.searchable(text: $context.searchQuery)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.searchable(text: $context.searchQuery)
|
||||
}
|
||||
.disabled(context.viewState.roomListMode == .skeletons)
|
||||
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
|
||||
.animation(.elementDefault, value: context.viewState.roomListMode)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.navigationTitle(ElementL10n.allChats)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
||||
@@ -24,42 +24,23 @@ enum MediaPlayerCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class MediaPlayerCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class MediaPlayerCoordinator: CoordinatorProtocol {
|
||||
private let parameters: MediaPlayerCoordinatorParameters
|
||||
private let mediaPlayerHostingController: UIViewController
|
||||
private var mediaPlayerViewModel: MediaPlayerViewModelProtocol
|
||||
private var viewModel: MediaPlayerViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((MediaPlayerCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: MediaPlayerCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL)
|
||||
let view = MediaPlayerScreen(context: viewModel.context)
|
||||
mediaPlayerViewModel = viewModel
|
||||
mediaPlayerHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mediaPlayerHostingController)
|
||||
viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
mediaPlayerViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("MediaPlayerViewModel did complete with result: \(action).")
|
||||
switch action {
|
||||
@@ -69,26 +50,7 @@ final class MediaPlayerCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
mediaPlayerHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(MediaPlayerScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
final class OnboardingCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: OnboardingViewModelProtocol
|
||||
|
||||
var callback: ((OnboardingCoordinatorAction) -> Void)?
|
||||
|
||||
init() {
|
||||
viewModel = OnboardingViewModel()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
viewModel.callback = { [weak self] action in
|
||||
MXLog.debug("OnboardingViewModel did complete with result: \(action).")
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .login:
|
||||
self.callback?(.login)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(OnboardingScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+22
-22
@@ -18,12 +18,12 @@ import SwiftUI
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum SplashScreenCoordinatorAction {
|
||||
enum OnboardingCoordinatorAction {
|
||||
case login
|
||||
}
|
||||
|
||||
/// The content displayed in a single splash screen page.
|
||||
struct SplashScreenPageContent {
|
||||
/// The content displayed in a single screen page.
|
||||
struct OnboardingPageContent {
|
||||
let title: AttributedString
|
||||
let message: String
|
||||
let image: ImageAsset
|
||||
@@ -31,13 +31,13 @@ struct SplashScreenPageContent {
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum SplashScreenViewModelAction {
|
||||
enum OnboardingViewModelAction {
|
||||
case login
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct SplashScreenViewState: BindableState {
|
||||
struct OnboardingViewState: BindableState {
|
||||
/// The colours of the background gradient shown behind the 4 pages.
|
||||
private let gradientColors = [
|
||||
Color(red: 0.95, green: 0.98, blue: 0.96),
|
||||
@@ -48,8 +48,8 @@ struct SplashScreenViewState: BindableState {
|
||||
]
|
||||
|
||||
/// An array containing all content of the carousel pages
|
||||
let content: [SplashScreenPageContent]
|
||||
var bindings: SplashScreenBindings
|
||||
let content: [OnboardingPageContent]
|
||||
var bindings: OnboardingBindings
|
||||
|
||||
/// The background gradient for all 4 pages and the hidden page at the start of the carousel.
|
||||
var backgroundGradient: Gradient {
|
||||
@@ -69,27 +69,27 @@ struct SplashScreenViewState: BindableState {
|
||||
let page4Title = locale.identifier.hasPrefix("en") ? "Cut the slack from teams." : ElementL10n.ftueAuthCarouselWorkplaceTitle
|
||||
|
||||
content = [
|
||||
SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselSecureTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselSecureBody,
|
||||
image: Asset.Images.splashScreenPage1),
|
||||
SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselControlTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselControlBody,
|
||||
image: Asset.Images.splashScreenPage2),
|
||||
SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselEncryptedTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselEncryptedBody,
|
||||
image: Asset.Images.splashScreenPage3),
|
||||
SplashScreenPageContent(title: page4Title.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName),
|
||||
image: Asset.Images.splashScreenPage4)
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselSecureTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselSecureBody,
|
||||
image: Asset.Images.onboardingScreenPage1),
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselControlTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselControlBody,
|
||||
image: Asset.Images.onboardingScreenPage2),
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselEncryptedTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselEncryptedBody,
|
||||
image: Asset.Images.onboardingScreenPage3),
|
||||
OnboardingPageContent(title: page4Title.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName),
|
||||
image: Asset.Images.onboardingScreenPage4)
|
||||
]
|
||||
bindings = SplashScreenBindings()
|
||||
bindings = OnboardingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
struct SplashScreenBindings {
|
||||
struct OnboardingBindings {
|
||||
var pageIndex = 0
|
||||
}
|
||||
|
||||
enum SplashScreenViewAction {
|
||||
enum OnboardingViewAction {
|
||||
case login
|
||||
}
|
||||
+5
-5
@@ -17,26 +17,26 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias SplashScreenViewModelType = StateStoreViewModel<SplashScreenViewState, SplashScreenViewAction>
|
||||
typealias OnboardingViewModelType = StateStoreViewModel<OnboardingViewState, OnboardingViewAction>
|
||||
|
||||
class SplashScreenViewModel: SplashScreenViewModelType, SplashScreenViewModelProtocol {
|
||||
class OnboardingViewModel: OnboardingViewModelType, OnboardingViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((SplashScreenViewModelAction) -> Void)?
|
||||
var callback: ((OnboardingViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: SplashScreenViewState())
|
||||
super.init(initialViewState: OnboardingViewState())
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: SplashScreenViewAction) async {
|
||||
override func process(viewAction: OnboardingViewAction) async {
|
||||
switch viewAction {
|
||||
case .login:
|
||||
callback?(.login)
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol SplashScreenViewModelProtocol {
|
||||
var callback: ((SplashScreenViewModelAction) -> Void)? { get set }
|
||||
var context: SplashScreenViewModelType.Context { get }
|
||||
protocol OnboardingViewModelProtocol {
|
||||
var callback: ((OnboardingViewModelAction) -> Void)? { get set }
|
||||
var context: OnboardingViewModelType.Context { get }
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplashScreenPageIndicator: View {
|
||||
struct OnboardingPageIndicator: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Public
|
||||
+5
-5
@@ -16,13 +16,13 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplashScreenPageView: View {
|
||||
struct OnboardingPageView: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// The content that this page should display.
|
||||
let content: SplashScreenPageContent
|
||||
let content: OnboardingPageContent
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
@@ -54,11 +54,11 @@ struct SplashScreenPageView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SplashScreenPage_Previews: PreviewProvider {
|
||||
static let content = SplashScreenViewState().content
|
||||
struct OnboardingPage_Previews: PreviewProvider {
|
||||
static let content = OnboardingViewState().content
|
||||
static var previews: some View {
|
||||
ForEach(0..<content.count, id: \.self) { index in
|
||||
SplashScreenPageView(content: content[index])
|
||||
OnboardingPageView(content: content[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -17,8 +17,8 @@
|
||||
import DesignKit
|
||||
import SwiftUI
|
||||
|
||||
/// The splash screen shown at the beginning of the onboarding flow.
|
||||
struct SplashScreen: View {
|
||||
/// The screen shown at the beginning of the onboarding flow.
|
||||
struct OnboardingScreen: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
@@ -36,7 +36,7 @@ struct SplashScreen: View {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: SplashScreenViewModel.Context
|
||||
@ObservedObject var context: OnboardingViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
@@ -47,12 +47,12 @@ struct SplashScreen: View {
|
||||
// The main content of the carousel
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Add a hidden page at the start of the carousel duplicating the content of the last page
|
||||
SplashScreenPageView(content: context.viewState.content[pageCount - 1])
|
||||
OnboardingPageView(content: context.viewState.content[pageCount - 1])
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier("hiddenPage")
|
||||
|
||||
ForEach(0..<pageCount, id: \.self) { index in
|
||||
SplashScreenPageView(content: context.viewState.content[index])
|
||||
OnboardingPageView(content: context.viewState.content[index])
|
||||
.frame(width: geometry.size.width)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ struct SplashScreen: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
SplashScreenPageIndicator(pageCount: pageCount, pageIndex: context.pageIndex)
|
||||
OnboardingPageIndicator(pageCount: pageCount, pageIndex: context.pageIndex)
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -213,11 +213,11 @@ struct SplashScreen: View {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct SplashScreen_Previews: PreviewProvider {
|
||||
static let viewModel = SplashScreenViewModel()
|
||||
struct OnboardingScreen_Previews: PreviewProvider {
|
||||
static let viewModel = OnboardingViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
SplashScreen(context: viewModel.context)
|
||||
OnboardingScreen(context: viewModel.context)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InviteFriendsCoordinator: CoordinatorProtocol {
|
||||
let userId: String
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userId).absoluteString else {
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink)
|
||||
|
||||
return AnyView(UIActivityViewControllerWrapper(activityItems: [shareText])
|
||||
.presentationDetents([.medium])
|
||||
.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -14,6 +14,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class SplashViewController: UIViewController { }
|
||||
struct SplashScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(
|
||||
Image(asset: Asset.Images.appLogo)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UIActivityViewControllerWrapper: UIViewControllerRepresentable {
|
||||
var activityItems: [Any]
|
||||
var applicationActivities: [UIActivity]?
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<UIActivityViewControllerWrapper>) -> UIActivityViewController {
|
||||
UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<UIActivityViewControllerWrapper>) { }
|
||||
}
|
||||
@@ -17,49 +17,33 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let navigationController: NavigationController
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomName: String?
|
||||
let roomAvatarUrl: String?
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: RoomScreenCoordinatorParameters
|
||||
private let roomScreenHostingController: UIViewController
|
||||
private var roomScreenViewModel: RoomScreenViewModelProtocol
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
// MARK: - Setup
|
||||
private var viewModel: RoomScreenViewModelProtocol
|
||||
private var navigationController: NavigationController { parameters.navigationController }
|
||||
|
||||
init(parameters: RoomScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
roomName: parameters.roomName,
|
||||
roomAvatarUrl: parameters.roomAvatarUrl)
|
||||
|
||||
let view = RoomScreen(context: viewModel.context)
|
||||
roomScreenViewModel = viewModel
|
||||
roomScreenHostingController = UIHostingController(rootView: view)
|
||||
viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
roomName: parameters.roomName,
|
||||
roomAvatarUrl: parameters.roomAvatarUrl)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
roomScreenViewModel.callback = { [weak self] result in
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("RoomScreenViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
@@ -71,12 +55,12 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
roomScreenHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
roomScreenViewModel.stop()
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(RoomScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -84,32 +68,20 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
private func displayVideo(for videoURL: URL) {
|
||||
let params = VideoPlayerCoordinatorParameters(videoURL: videoURL)
|
||||
let coordinator = VideoPlayerCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self, weak coordinator] _ in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.popModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationController.pop()
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func displayFile(for fileURL: URL, with title: String?) {
|
||||
let params = FilePreviewCoordinatorParameters(fileURL: fileURL, title: title)
|
||||
let coordinator = FilePreviewCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self, weak coordinator] _ in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.popModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationController.pop()
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,37 +20,22 @@ struct SessionVerificationCoordinatorParameters {
|
||||
let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
|
||||
}
|
||||
|
||||
final class SessionVerificationCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class SessionVerificationCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SessionVerificationCoordinatorParameters
|
||||
private let sessionVerificationHostingController: UIViewController
|
||||
private var sessionVerificationViewModel: SessionVerificationViewModelProtocol
|
||||
private var viewModel: SessionVerificationViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: SessionVerificationCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy)
|
||||
let view = SessionVerificationScreen(context: viewModel.context)
|
||||
sessionVerificationViewModel = viewModel
|
||||
sessionVerificationHostingController = UIHostingController(rootView: view)
|
||||
viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
sessionVerificationViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
@@ -60,9 +45,7 @@ final class SessionVerificationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
sessionVerificationHostingController
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SessionVerificationScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let navigationController: NavigationController
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
}
|
||||
@@ -27,25 +28,10 @@ enum SettingsCoordinatorAction {
|
||||
case logout
|
||||
}
|
||||
|
||||
final class SettingsCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class SettingsCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SettingsCoordinatorParameters
|
||||
private let settingsHostingController: UIViewController
|
||||
private var settingsViewModel: SettingsViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var statusIndicator: UserIndicator?
|
||||
private var viewModel: SettingsViewModelProtocol
|
||||
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((SettingsCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
@@ -53,14 +39,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
init(parameters: SettingsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = SettingsViewModel(withUserSession: parameters.userSession)
|
||||
let view = SettingsScreen(context: viewModel.context)
|
||||
settingsViewModel = viewModel
|
||||
settingsHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: settingsHostingController)
|
||||
|
||||
settingsViewModel.callback = { [weak self] result in
|
||||
viewModel = SettingsViewModel(withUserSession: parameters.userSession)
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("SettingsViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
@@ -80,16 +60,10 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
// no-op
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SettingsScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
settingsHostingController
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func toggleAnalytics() {
|
||||
@@ -102,39 +76,25 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
|
||||
private func presentBugReportScreen() {
|
||||
let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService,
|
||||
screenshot: nil)
|
||||
userNotificationController: parameters.userNotificationController,
|
||||
screenshot: nil,
|
||||
isModallyPresented: false)
|
||||
let coordinator = BugReportCoordinator(parameters: params)
|
||||
coordinator.completion = { [weak self, weak coordinator] in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.parameters.navigationRouter.popModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.showSuccess(label: ElementL10n.done)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
self.remove(childCoordinator: coordinator)
|
||||
coordinator.completion = { [weak self] result in
|
||||
switch result {
|
||||
case .finish:
|
||||
self?.showSuccess(label: ElementL10n.done)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self?.parameters.navigationController.pop()
|
||||
}
|
||||
|
||||
parameters.navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Show success indicator
|
||||
private func showSuccess(label: String) {
|
||||
statusIndicator = indicatorPresenter.present(.success(label: label))
|
||||
parameters.userNotificationController.submitNotification(UserNotification(title: label))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias SettingsViewModelType = StateStoreViewModel<SettingsViewState,
|
||||
SettingsViewAction>
|
||||
@available(iOS 14, *)
|
||||
typealias SettingsViewModelType = StateStoreViewModel<SettingsViewState, SettingsViewAction>
|
||||
|
||||
class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
@@ -19,6 +19,5 @@ import Foundation
|
||||
@MainActor
|
||||
protocol SettingsViewModelProtocol {
|
||||
var callback: ((SettingsViewModelAction) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
var context: SettingsViewModelType.Context { get }
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SplashViewController" customModule="ElementX" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Images/app-logo" translatesAutoresizingMaskIntoConstraints="NO" id="ue7-fB-5XS">
|
||||
<rect key="frame" x="87" y="328" width="240" height="240"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ue7-fB-5XS" secondAttribute="bottom" constant="16" id="Cip-Sc-gaF"/>
|
||||
<constraint firstItem="ue7-fB-5XS" firstAttribute="top" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="16" id="Clt-cS-YAr"/>
|
||||
<constraint firstItem="ue7-fB-5XS" firstAttribute="centerY" secondItem="i5M-Pr-FkT" secondAttribute="centerY" id="N3w-Jf-MRA"/>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="ue7-fB-5XS" secondAttribute="trailing" constant="16" id="WfN-3K-kpr"/>
|
||||
<constraint firstItem="ue7-fB-5XS" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="ujr-SL-AyX"/>
|
||||
<constraint firstItem="ue7-fB-5XS" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="16" id="yNl-Wu-AES"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="137.68115942028987" y="153.34821428571428"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="Images/app-logo" width="240" height="240"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
final class SplashScreenCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let splashScreenHostingController: UIViewController
|
||||
private var splashScreenViewModel: SplashScreenViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((SplashScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init() {
|
||||
let viewModel = SplashScreenViewModel()
|
||||
let view = SplashScreen(context: viewModel.context)
|
||||
splashScreenViewModel = viewModel
|
||||
splashScreenHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: splashScreenHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
splashScreenViewModel.callback = { [weak self] action in
|
||||
MXLog.debug("SplashScreenViewModel did complete with result: \(action).")
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .login:
|
||||
self.callback?(.login)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
splashScreenHostingController
|
||||
}
|
||||
|
||||
/// Stops any ongoing activities in the coordinator.
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
}
|
||||
@@ -25,45 +25,24 @@ enum VideoPlayerCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
final class VideoPlayerCoordinator: CoordinatorProtocol {
|
||||
private let parameters: VideoPlayerCoordinatorParameters
|
||||
private let videoPlayerHostingController: UIViewController
|
||||
private var videoPlayerViewModel: VideoPlayerViewModelProtocol
|
||||
private var viewModel: VideoPlayerViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((VideoPlayerCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: VideoPlayerCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL)
|
||||
let view = VideoPlayerScreen(context: viewModel.context)
|
||||
videoPlayerViewModel = viewModel
|
||||
videoPlayerHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: videoPlayerHostingController)
|
||||
viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
configureAudioSession(.sharedInstance())
|
||||
|
||||
videoPlayerViewModel.callback = { [weak self] action in
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("VideoPlayerViewModel did complete with result: \(action).")
|
||||
switch action {
|
||||
@@ -73,16 +52,12 @@ final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
videoPlayerHostingController
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(VideoPlayerScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private func configureAudioSession(_ session: AVAudioSession) {
|
||||
do {
|
||||
try session.setCategory(.playback,
|
||||
@@ -93,17 +68,4 @@ final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
MXLog.debug("Configure audio session failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user