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 authored Jun 30, 2022
1 parent dd739d4 commit baeffd2
Show file tree
Hide file tree
Showing 32 changed files with 710 additions and 40 deletions.
52 changes: 50 additions & 2 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

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 @@ -74,7 +74,7 @@ final class LoginCoordinator: Coordinator, Presentable {
let viewModel = LoginViewModel(homeserver: parameters.homeserver)
loginViewModel = viewModel

let view = LoginScreen(viewModel: viewModel.context)
let view = LoginScreen(context: viewModel.context)
loginHostingController = UIHostingController(rootView: view)

indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController)
Expand Down 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: ElementNavigationController())
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
Expand Up @@ -29,7 +29,7 @@ struct LoginScreen: View {

// MARK: Public

@ObservedObject var viewModel: LoginViewModel.Context
@ObservedObject var context: LoginViewModel.Context

var body: some View {
ScrollView {
Expand All @@ -46,7 +46,7 @@ struct LoginScreen: View {
.frame(height: 1)
.padding(.vertical, 21)

switch viewModel.viewState.loginMode {
switch context.viewState.loginMode {
case .password:
loginForm
case .oidc:
Expand All @@ -61,7 +61,7 @@ struct LoginScreen: View {
.padding(.bottom, 16)
}
.background(Color.element.background.ignoresSafeArea())
.alert(item: $viewModel.alertInfo) { $0.alert }
.alert(item: $context.alertInfo) { $0.alert }
}

/// The header containing a Welcome Back title.
Expand All @@ -74,16 +74,16 @@ struct LoginScreen: View {

/// The sever information section that includes a button to select a different server.
var serverInfo: some View {
LoginServerInfoSection(address: viewModel.viewState.homeserver.address,
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
viewModel.send(viewAction: .selectServer)
LoginServerInfoSection(address: context.viewState.homeserver.address,
showMatrixDotOrgInfo: context.viewState.homeserver.isMatrixDotOrg) {
context.send(viewAction: .selectServer)
}
}

/// The form with text fields for username and password, along with a submit button.
var loginForm: some View {
VStack(spacing: 14) {
TextField(ElementL10n.loginSigninUsernameHint, text: $viewModel.username)
TextField(ElementL10n.loginSigninUsernameHint, text: $context.username)
.focused($isUsernameFocused)
.textFieldStyle(.elementInput())
.disableAutocorrection(true)
Expand All @@ -96,15 +96,15 @@ struct LoginScreen: View {

Spacer().frame(height: 20)

SecureField(ElementL10n.loginSignupPasswordHint, text: $viewModel.password)
SecureField(ElementL10n.loginSignupPasswordHint, text: $context.password)
.focused($isPasswordFocused)
.textFieldStyle(.elementInput())
.textContentType(.password)
.submitLabel(.done)
.onSubmit(submit)
.accessibilityIdentifier("passwordTextField")

Button { viewModel.send(viewAction: .forgotPassword) } label: {
Button { context.send(viewAction: .forgotPassword) } label: {
Text(ElementL10n.authenticationLoginForgotPassword)
.font(.element.body)
}
Expand All @@ -115,14 +115,14 @@ struct LoginScreen: View {
Text(ElementL10n.loginSignupSubmit)
}
.buttonStyle(.elementAction(.xLarge))
.disabled(!viewModel.viewState.canSubmit)
.disabled(!context.viewState.canSubmit)
.accessibilityIdentifier("nextButton")
}
}

/// The OIDC button that can be used for login.
var oidcButton: some View {
Button { viewModel.send(viewAction: .continueWithOIDC) } label: {
Button { context.send(viewAction: .continueWithOIDC) } label: {
Text(ElementL10n.loginContinue)
}
.buttonStyle(.elementAction(.xLarge))
Expand All @@ -141,14 +141,14 @@ struct LoginScreen: View {

/// Parses the username for a homeserver.
private func usernameFocusChanged(isFocussed: Bool) {
guard !isFocussed, !viewModel.username.isEmpty else { return }
viewModel.send(viewAction: .parseUsername)
guard !isFocussed, !context.username.isEmpty else { return }
context.send(viewAction: .parseUsername)
}

/// Sends the `next` view action so long as valid credentials have been input.
private func submit() {
guard viewModel.viewState.canSubmit else { return }
viewModel.send(viewAction: .next)
guard context.viewState.canSubmit else { return }
context.send(viewAction: .next)
}
}

Expand All @@ -171,7 +171,7 @@ struct Login_Previews: PreviewProvider {

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

import SwiftUI

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

/// Generate the view struct for the screen state.
@MainActor var viewModel: ServerSelectionViewModel {
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)
}
}
}
Loading

0 comments on commit baeffd2

Please sign in to comment.