Skip to content

Commit

Permalink
#40: Add server selection screen from EI.
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave committed Jun 29, 2022
1 parent d74158c commit 50f4b67
Show file tree
Hide file tree
Showing 30 changed files with 796 additions and 13 deletions.
68 changes: 60 additions & 8 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"location" : "https://github.com/matrix-org/matrix-rust-components-swift.git",
"state" : {
"branch" : "main",
"revision" : "43c88a4b0912a1589c2a28cc9bb2df45c70cdcad"
"revision" : "166966cb524f899bf0ac95e840c0fe01de48123e"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "authentication_server_selection_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions ElementX/Resources/Localizations/en.lproj/Untranslated.strings
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
/* Used for testing */
"untranslated" = "Untranslated";

"action_confirm" = "Confirm";
"action_next" = "Next";

"screenshot_detected_title" = "You took a screenshot";
"screenshot_detected_message" = "Would you like to submit a bug report?";

Expand All @@ -13,3 +18,9 @@

"authentication_server_info_title" = "Choose your server to store your data";
"authentication_server_info_matrix_description" = "Join millions for free on the largest public server";

"server_selection_title" = "Choose your server";
"server_selection_message" = "What is the address of your server? A server is like a home for all your data.";
"server_selection_server_url" = "Server URL";
"server_selection_server_footer" = "You can only connect to a server that has already been set up";
"server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
7 changes: 4 additions & 3 deletions ElementX/Sources/Generated/Assets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
internal enum Asset {
internal enum Images {
internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal")
internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted")
internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning")
internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon")
internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1")
internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2")
internal static let splashScreenPage3 = ImageAsset(name: "Images/Splash Screen Page 3")
internal static let splashScreenPage4 = ImageAsset(name: "Images/Splash Screen Page 4")
internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal")
internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted")
internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning")
internal static let appLogo = ImageAsset(name: "Images/app-logo")
internal static let closeCircle = ImageAsset(name: "Images/close_circle")
internal static let timelineComposerSendMessage = ImageAsset(name: "Images/timelineComposerSendMessage")
Expand Down
14 changes: 14 additions & 0 deletions ElementX/Sources/Generated/Strings+Untranslated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
extension ElementL10n {
/// Confirm
public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm")
/// Next
public static let actionNext = ElementL10n.tr("Untranslated", "action_next")
/// Forgot password
public static let authenticationLoginForgotPassword = ElementL10n.tr("Untranslated", "authentication_login_forgot_password")
/// Welcome back!
Expand All @@ -26,6 +30,16 @@ extension ElementL10n {
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
/// You took a screenshot
public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title")
/// Cannot find a server at this URL, please check it is correct.
public static let serverSelectionGenericError = ElementL10n.tr("Untranslated", "server_selection_generic_error")
/// What is the address of your server? A server is like a home for all your data.
public static let serverSelectionMessage = ElementL10n.tr("Untranslated", "server_selection_message")
/// You can only connect to a server that has already been set up
public static let serverSelectionServerFooter = ElementL10n.tr("Untranslated", "server_selection_server_footer")
/// Server URL
public static let serverSelectionServerUrl = ElementL10n.tr("Untranslated", "server_selection_server_url")
/// Choose your server
public static let serverSelectionTitle = ElementL10n.tr("Untranslated", "server_selection_title")
/// Timeline Style
public static let settingsTimelineStyle = ElementL10n.tr("Untranslated", "settings_timeline_style")
/// Untranslated
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// 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

/// An image that is styled for use as the screen icon in the onboarding flow.
struct AuthenticationIconImage: View {

let image: ImageAsset

var body: some View {
Image(image.name)
.resizable()
.renderingMode(.template)
.foregroundColor(.element.accent)
.frame(width: 90, height: 90)
.background(.white, in: Circle().inset(by: 2))
.accessibilityHidden(true)
}
}

// MARK: - Previews

struct AuthenticationIconImage_Previews: PreviewProvider {
static var previews: some View {
AuthenticationIconImage(image: Asset.Images.serverSelectionIcon)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,34 @@ final class LoginCoordinator: Coordinator, Presentable {

/// Presents the server selection screen as a modal.
private func presentServerSelectionScreen() {
loginViewModel.displayError(.alert("Not implemented. Enter a full Matrix ID such as @user:server.com"))
MXLog.debug("[LoginCoordinator] presentServerSelectionScreen")
let parameters = ServerSelectionCoordinatorParameters(homeserver: loginViewModel.context.viewState.homeserver,
hasModalPresentation: true)
let coordinator = ServerSelectionCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
self.serverSelectionCoordinator(coordinator, didCompleteWith: result)
}

coordinator.start()
add(childCoordinator: coordinator)

let modalRouter = NavigationRouter(navigationController: UINavigationController())
modalRouter.setRootModule(coordinator)

navigationRouter.present(modalRouter, animated: true)
}

/// Handles the result from the server selection modal, dismissing it after updating the view.
private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator,
didCompleteWith result: ServerSelectionCoordinatorResult) {
navigationRouter.dismissModule(animated: true) { [weak self] in
if case let .selected(homeserver) = result {
self?.updateViewModel(homeserver: homeserver)
}

self?.remove(childCoordinator: coordinator)
}
}

/// Shows the forgot password screen.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

enum MockServerSelectionScreenState: CaseIterable {
case matrix
case emptyAddress
case invalidAddress
case nonModal

/// Generate the view struct for the screen state.
var viewModel: ServerSelectionViewModel {
// swiftlint:disable:next implicit_getter
@MainActor get {
switch self {
case .matrix:
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
hasModalPresentation: true)
case .emptyAddress:
return ServerSelectionViewModel(homeserverAddress: "",
hasModalPresentation: true)
case .invalidAddress:
let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad",
hasModalPresentation: true)
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
return viewModel
case .nonModal:
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
hasModalPresentation: false)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

struct ServerSelectionCoordinatorParameters {
/// The homeserver to be shown initially.
let homeserver: LoginHomeserver
/// Whether the screen is presented modally or within a navigation stack.
let hasModalPresentation: Bool
}

enum ServerSelectionCoordinatorResult {
case selected(LoginHomeserver)
case dismiss
}

final class ServerSelectionCoordinator: Coordinator, Presentable {

// MARK: - Properties

// MARK: Private

private let parameters: ServerSelectionCoordinatorParameters
private let serverSelectionHostingController: UIViewController
private var serverSelectionViewModel: ServerSelectionViewModelProtocol

private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?

// MARK: Public

// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: (@MainActor (ServerSelectionCoordinatorResult) -> Void)?

// MARK: - Setup

init(parameters: ServerSelectionCoordinatorParameters) {
self.parameters = parameters

let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.homeserver.address,
hasModalPresentation: parameters.hasModalPresentation)
let view = ServerSelectionScreen(viewModel: viewModel.context)
serverSelectionViewModel = viewModel
serverSelectionHostingController = UIHostingController(rootView: view)

indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController)
}

// MARK: - Public

func start() {
MXLog.debug("[ServerSelectionCoordinator] did start.")

serverSelectionViewModel.callback = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did complete with result: \(result).")

switch result {
case .confirm(let homeserverAddress):
self.useHomeserver(homeserverAddress)
case .dismiss:
self.callback?(.dismiss)
}
}
}

func toPresentable() -> UIViewController {
return self.serverSelectionHostingController
}

// 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))
}

/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}

/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
private func useHomeserver(_ homeserverAddress: String) {
startLoading()

let homeserverAddress = LoginHomeserver.sanitized(homeserverAddress)
callback?(.selected(LoginHomeserver(address: homeserverAddress)))

stopLoading()
}
}
Loading

0 comments on commit 50f4b67

Please sign in to comment.