Add a view to show the user's account from Element Classic. (#5361)

No presentation logic yet.
This commit is contained in:
Doug
2026-04-08 21:18:03 +01:00
committed by GitHub
parent f21e8cb892
commit 1a5a709e27
19 changed files with 588 additions and 26 deletions
@@ -14,9 +14,20 @@ struct AuthenticationStartScreenParameters {
let provisioningParameters: AccountProvisioningParameters?
let isBugReportServiceEnabled: Bool
let appSettings: AppSettings
let mediaProvider: MediaProviderProtocol?
let userIndicatorController: UserIndicatorControllerProtocol
}
enum AuthenticationStartScreenCoordinatorAction {
case loginWithQR
case login
case register
case reportProblem
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
case loginDirectlyWithPassword(loginHint: String?)
}
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
private var viewModel: AuthenticationStartScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
@@ -31,6 +42,7 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
provisioningParameters: parameters.provisioningParameters,
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
appSettings: parameters.appSettings,
mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController)
}
@@ -8,18 +8,6 @@
import SwiftUI
// MARK: - Coordinator
enum AuthenticationStartScreenCoordinatorAction {
case loginWithQR
case login
case register
case reportProblem
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
case loginDirectlyWithPassword(loginHint: String?)
}
enum AuthenticationStartScreenViewModelAction: Equatable {
case loginWithQR
case login
@@ -70,4 +58,8 @@ enum AuthenticationStartScreenViewAction {
case login
case register
case reportProblem
case continueWithClassic(ClassicAppAccount)
case otherOptions(ClassicAppAccount)
case closeOtherOptions(ClassicAppAccount)
}
@@ -24,11 +24,12 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(authenticationService: AuthenticationServiceProtocol,
provisioningParameters: AccountProvisioningParameters?,
isBugReportServiceEnabled: Bool,
appSettings: AppSettings,
mediaProvider: MediaProviderProtocol?,
userIndicatorController: UserIndicatorControllerProtocol) {
self.authenticationService = authenticationService
self.provisioningParameters = provisioningParameters
@@ -59,9 +60,9 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
hideBrandChrome: appSettings.hideBrandChrome)
}
super.init(initialViewState: initialViewState)
super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
}
override func process(viewAction: AuthenticationStartScreenViewAction) {
switch viewAction {
case .updateWindow(let window):
@@ -77,6 +78,8 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
if canReportProblem {
actionsSubject.send(.reportProblem)
}
case .continueWithClassic, .otherOptions, .closeOtherOptions:
break // To follow.
}
}
@@ -0,0 +1,120 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct AuthenticationClassicAppAccountView: View {
@Bindable var context: AuthenticationStartScreenViewModel.Context
let classicAppAccount: ClassicAppAccount
var body: some View {
FullscreenDialog(topPadding: 25, background: .gradient) {
VStack(spacing: 38) {
header
.padding(.bottom, 20)
profile
buttons
}
} bottomContent: {
// Buttons are intentionally shown inline on this screen.
}
.navigationBarTitleDisplayMode(.inline)
.alert(item: $context.alertInfo)
.introspect(.window, on: .supportedVersions) { window in
context.send(viewAction: .updateWindow(window))
}
}
var header: some View {
VStack(spacing: 8) {
AuthenticationStartLogo(size: 54, hideBrandChrome: false, isOnGradient: false)
Text(L10n.screenOnboardingWelcomeTitle)
.font(.compound.headingMDBold)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
}
}
var profile: some View {
VStack(spacing: 16) {
LoadableAvatarImage(url: classicAppAccount.avatarURL,
name: classicAppAccount.displayName,
contentID: classicAppAccount.userID,
avatarSize: .user(on: .classicAppAccount),
mediaProvider: context.mediaProvider)
VStack(spacing: 0) {
Text(L10n.screenOnboardingWelcomeBack)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.multilineTextAlignment(.center)
Text(classicAppAccount.displayableName)
.font(.compound.headingLGBold)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
}
Text(classicAppAccount.userID)
.font(.compound.bodyLGSemibold)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
}
}
var buttons: some View {
VStack(spacing: 16) {
Button(L10n.actionContinue) {
context.send(viewAction: .continueWithClassic(classicAppAccount))
}
.buttonStyle(.compound(.primary))
Button(L10n.commonOtherOptions) {
context.send(viewAction: .otherOptions(classicAppAccount))
}
.buttonStyle(.compound(.secondary))
}
}
}
private extension ClassicAppAccount {
var displayableName: String {
if let displayName, !displayName.isEmpty {
displayName
} else if let localPart = userID.dropFirst().split(separator: ":").first, !localPart.isEmpty {
String(localPart)
} else {
userID
}
}
}
// MARK: - Previews
struct AuthenticationClassicAppAccountView_Previews: PreviewProvider { // Not Testable – snapshots generated by main screen.
static let viewModel = makeViewModel()
static var previews: some View {
ElementNavigationStack {
AuthenticationClassicAppAccountView(context: viewModel.context, classicAppAccount: .mockDan)
}
}
static func makeViewModel() -> AuthenticationStartScreenViewModel {
AuthenticationStartScreenViewModel(authenticationService: AuthenticationService.mock,
provisioningParameters: nil,
isBugReportServiceEnabled: false,
appSettings: ServiceLocator.shared.settings,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}
}
@@ -69,6 +69,7 @@ struct AuthenticationClassicAppBackupInstructionsView_Previews: PreviewProvider,
provisioningParameters: nil,
isBugReportServiceEnabled: false,
appSettings: ServiceLocator.shared.settings,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}
}
@@ -152,6 +152,7 @@ struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
provisioningParameters: provisionedServerName.map { .init(accountProvider: $0, loginHint: nil) },
isBugReportServiceEnabled: true,
appSettings: ServiceLocator.shared.settings,
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}
}