Add a Link New Device screen (behind a feature flag). (#4811)

This commit is contained in:
Doug
2025-12-11 11:45:23 +00:00
committed by GitHub
parent 9ffb9a22e3
commit 4ea1aaecdd
46 changed files with 635 additions and 27 deletions
@@ -66,6 +66,7 @@ final class AppSettings {
case linkPreviewsEnabled
case spaceSettingsEnabled
case focusEventOnNotificationTap
case linkNewDeviceEnabled
// Doug's tweaks 🔧
case hideUnreadMessagesBadge
@@ -399,6 +400,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.linkPreviewsEnabled, defaultValue: false, storageType: .userDefaults(store))
var linkPreviewsEnabled
@UserPreference(key: UserDefaultsKeys.linkNewDeviceEnabled, defaultValue: false, storageType: .userDefaults(store))
var linkNewDeviceEnabled
@UserPreference(key: UserDefaultsKeys.developerOptionsEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store))
var developerOptionsEnabled
}
@@ -83,6 +83,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
startEncryptionSettingsFlow(animated: true)
case .userDetails:
presentUserDetailsEditScreen()
case .linkNewDevice:
presentLinkNewDeviceScreen()
case let .manageAccount(url):
presentAccountManagementURL(url)
case .analytics:
@@ -269,4 +271,27 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
appSettings: flowParameters.appSettings)
accountSettingsPresenter?.start()
}
// MARK: - Link New Device
private func presentLinkNewDeviceScreen() {
let parameters = LinkNewDeviceScreenCoordinatorParameters(clientProxy: flowParameters.userSession.clientProxy)
let coordinator = LinkNewDeviceScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .linkMobileDevice:
break
case .linkDesktopComputer:
break
case .dismiss:
navigationStackCoordinator.pop()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
}
+67 -5
View File
@@ -334,6 +334,8 @@ internal enum L10n {
internal static var actionStart: String { return L10n.tr("Localizable", "action_start") }
/// Start chat
internal static var actionStartChat: String { return L10n.tr("Localizable", "action_start_chat") }
/// Start over
internal static var actionStartOver: String { return L10n.tr("Localizable", "action_start_over") }
/// Start verification
internal static var actionStartVerification: String { return L10n.tr("Localizable", "action_start_verification") }
/// Tap to load map
@@ -1888,12 +1890,72 @@ internal enum L10n {
internal static func screenLeaveSpaceTitleLastAdmin(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_leave_space_title_last_admin", String(describing: p1))
}
/// Scan the QR code
internal static var screenLinkNewDeviceDesktopScanningTitle: String { return L10n.tr("Localizable", "screen_link_new_device_desktop_scanning_title") }
/// Open %1$@ on a laptop or desktop computer
internal static func screenLinkNewDeviceDesktopStep1(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_desktop_step1", String(describing: p1))
}
/// Scan the QR code with this device
internal static var screenLinkNewDeviceDesktopStep3: String { return L10n.tr("Localizable", "screen_link_new_device_desktop_step3") }
/// Ready to scan
internal static var screenLinkNewDeviceDesktopSubmit: String { return L10n.tr("Localizable", "screen_link_new_device_desktop_submit") }
/// Open %1$@ on a desktop computer to get the QR code
internal static func screenLinkNewDeviceDesktopTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_desktop_title", String(describing: p1))
}
/// The numbers don’t match
internal static var screenLinkNewDeviceEnterNumberErrorNumbersDoNotMatch: String { return L10n.tr("Localizable", "screen_link_new_device_enter_number_error_numbers_do_not_match") }
/// Enter 2-digit code
internal static var screenLinkNewDeviceEnterNumberNotice: String { return L10n.tr("Localizable", "screen_link_new_device_enter_number_notice") }
/// This will verify that the connection to your other device is secure.
internal static var screenLinkNewDeviceEnterNumberSubtitle: String { return L10n.tr("Localizable", "screen_link_new_device_enter_number_subtitle") }
/// Enter the number shown on your other device
internal static var screenLinkNewDeviceEnterNumberTitle: String { return L10n.tr("Localizable", "screen_link_new_device_enter_number_title") }
/// Your account provider does not support %1$@.
internal static func screenLinkNewDeviceErrorAppNotSupportedSubtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_error_app_not_supported_subtitle", String(describing: p1))
}
/// %1$@ not supported
internal static func screenLinkNewDeviceErrorAppNotSupportedTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_error_app_not_supported_title", String(describing: p1))
}
/// Your account provider doesn’t support signing into a new device with a QR code.
internal static var screenLinkNewDeviceErrorNotSupportedSubtitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_not_supported_subtitle") }
/// QR code not supported
internal static var screenLinkNewDeviceErrorNotSupportedTitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_not_supported_title") }
/// The sign in was cancelled on the other device.
internal static var screenLinkNewDeviceErrorRequestCancelledSubtitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_request_cancelled_subtitle") }
/// Sign in request cancelled
internal static var screenLinkNewDeviceErrorRequestCancelledTitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_request_cancelled_title") }
/// Sign in expired. Please try again.
internal static var screenLinkNewDeviceErrorRequestTimeoutSubtitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_request_timeout_subtitle") }
/// The sign in was not completed in time
internal static var screenLinkNewDeviceErrorRequestTimeoutTitle: String { return L10n.tr("Localizable", "screen_link_new_device_error_request_timeout_title") }
/// Open %1$@ on the other device
internal static func screenLinkNewDeviceMobileStep1(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_mobile_step1", String(describing: p1))
}
/// Select %1$@
internal static func screenLinkNewDeviceMobileStep2(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_mobile_step2", String(describing: p1))
}
/// “Sign in with QR code”
internal static var screenLinkNewDeviceMobileStep2Action: String { return L10n.tr("Localizable", "screen_link_new_device_mobile_step2_action") }
/// Scan the QR code shown here with the other device
internal static var screenLinkNewDeviceMobileStep3: String { return L10n.tr("Localizable", "screen_link_new_device_mobile_step3") }
/// Open %1$@ on the other device
internal static func screenLinkNewDeviceMobileTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_link_new_device_mobile_title", String(describing: p1))
}
/// Desktop computer
internal static var screenLinkNewDeviceDesktopComputer: String { return L10n.tr("Localizable", "screen_link_new_device_desktop_computer") }
internal static var screenLinkNewDeviceRootDesktopComputer: String { return L10n.tr("Localizable", "screen_link_new_device_root_desktop_computer") }
/// Loading QR code…
internal static var screenLinkNewDeviceRootLoadingQrCode: String { return L10n.tr("Localizable", "screen_link_new_device_root_loading_qr_code") }
/// Mobile device
internal static var screenLinkNewDeviceMobileDevice: String { return L10n.tr("Localizable", "screen_link_new_device_mobile_device") }
internal static var screenLinkNewDeviceRootMobileDevice: String { return L10n.tr("Localizable", "screen_link_new_device_root_mobile_device") }
/// What type of device do you want to link?
internal static var screenLinkNewDeviceTitle: String { return L10n.tr("Localizable", "screen_link_new_device_title") }
internal static var screenLinkNewDeviceRootTitle: String { return L10n.tr("Localizable", "screen_link_new_device_root_title") }
/// This account has been deactivated.
internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") }
/// Incorrect username and/or password
@@ -2782,7 +2844,7 @@ internal enum L10n {
internal static var screenRoomlistTombstonedRoomDescription: String { return L10n.tr("Localizable", "screen_roomlist_tombstoned_room_description") }
/// Add address
internal static var screenSecurityAndPrivacyAddRoomAddressAction: String { return L10n.tr("Localizable", "screen_security_and_privacy_add_room_address_action") }
/// Anyone in authorized spaces can join, but everyone else must request access.
/// Anyone in authorised spaces can join, but everyone else must request access.
internal static var screenSecurityAndPrivacyAskToJoinMultipleSpacesMembersOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description") }
/// Everyone must request access.
internal static var screenSecurityAndPrivacyAskToJoinOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_ask_to_join_option_description") }
@@ -2822,7 +2884,7 @@ internal enum L10n {
internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_title") }
/// Access
internal static var screenSecurityAndPrivacyRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_section_header") }
/// Anyone in authorized spaces can join.
/// Anyone in authorised spaces can join.
internal static var screenSecurityAndPrivacyRoomAccessSpaceMembersOptionMultipleParentsDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_space_members_option_multiple_parents_description") }
/// Anyone in %1$@ can join.
internal static func screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(_ p1: Any) -> String {
@@ -144,6 +144,7 @@ extension ClientProxyMock {
underlyingIsReportRoomSupported = true
underlyingIsLiveKitRTCSupported = true
underlyingIsLoginWithQRCodeSupported = true
underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject<TimelineMediaVisibility, Never>(configuration.timelineMediaVisibility).asCurrentValuePublisher()
underlyingHideInviteAvatarsPublisher = CurrentValueSubject<Bool, Never>(configuration.hideInviteAvatars).asCurrentValuePublisher()
@@ -2332,6 +2332,23 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
var underlyingIsLiveKitRTCSupported: Bool!
var isLiveKitRTCSupportedClosure: (() async -> Bool)?
var isLoginWithQRCodeSupportedCallsCount = 0
var isLoginWithQRCodeSupportedCalled: Bool {
return isLoginWithQRCodeSupportedCallsCount > 0
}
var isLoginWithQRCodeSupported: Bool {
get async {
isLoginWithQRCodeSupportedCallsCount += 1
if let isLoginWithQRCodeSupportedClosure = isLoginWithQRCodeSupportedClosure {
return await isLoginWithQRCodeSupportedClosure()
} else {
return underlyingIsLoginWithQRCodeSupported
}
}
}
var underlyingIsLoginWithQRCodeSupported: Bool!
var isLoginWithQRCodeSupportedClosure: (() async -> Bool)?
var maxMediaUploadSizeCallsCount = 0
var maxMediaUploadSizeCalled: Bool {
return maxMediaUploadSizeCallsCount > 0
@@ -77,6 +77,7 @@ enum TestablePreviewsDictionary {
"LeaveSpaceRoomDetailsCell_Previews" : LeaveSpaceRoomDetailsCell_Previews.self,
"LeaveSpaceView_Previews" : LeaveSpaceView_Previews.self,
"LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self,
"LinkNewDeviceScreen_Previews" : LinkNewDeviceScreen_Previews.self,
"LoadableImage_Previews" : LoadableImage_Previews.self,
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
"LocationRoomTimelineView_Previews" : LocationRoomTimelineView_Previews.self,
@@ -0,0 +1,55 @@
//
// 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 Combine
import SwiftUI
enum LinkNewDeviceScreenCoordinatorAction {
case linkMobileDevice
case linkDesktopComputer
case dismiss
}
struct LinkNewDeviceScreenCoordinatorParameters {
let clientProxy: ClientProxyProtocol
}
final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol {
private let viewModel: LinkNewDeviceScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<LinkNewDeviceScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<LinkNewDeviceScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: LinkNewDeviceScreenCoordinatorParameters) {
viewModel = LinkNewDeviceScreenViewModel(clientProxy: parameters.clientProxy)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .linkMobileDevice:
actionsSubject.send(.linkMobileDevice)
case .linkDesktopComputer:
actionsSubject.send(.linkDesktopComputer)
case .dismiss:
actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(LinkNewDeviceScreen(context: viewModel.context))
}
}
@@ -0,0 +1,25 @@
//
// 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 Foundation
enum LinkNewDeviceScreenViewModelAction {
case linkMobileDevice
case linkDesktopComputer
case dismiss
}
struct LinkNewDeviceScreenViewState: BindableState {
enum Mode: Equatable { case loading, readyToLink(isGeneratingCode: Bool), notSupported }
var mode: Mode = .loading
}
enum LinkNewDeviceScreenViewAction {
case linkMobileDevice
case linkDesktopComputer
case dismiss
}
@@ -0,0 +1,61 @@
//
// 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 Combine
import SwiftUI
typealias LinkNewDeviceScreenViewModelType = StateStoreViewModelV2<LinkNewDeviceScreenViewState, LinkNewDeviceScreenViewAction>
class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDeviceScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
private let actionsSubject: PassthroughSubject<LinkNewDeviceScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<LinkNewDeviceScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(clientProxy: ClientProxyProtocol) {
self.clientProxy = clientProxy
super.init(initialViewState: LinkNewDeviceScreenViewState())
Task { await checkQRCodeLoginSupport() }
}
// MARK: - Public
override func process(viewAction: LinkNewDeviceScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .linkMobileDevice:
linkMobileDevice()
case .linkDesktopComputer:
actionsSubject.send(.linkDesktopComputer)
case .dismiss:
actionsSubject.send(.dismiss)
}
}
// MARK: - Private
private func checkQRCodeLoginSupport() async {
if await clientProxy.isLoginWithQRCodeSupported {
state.mode = .readyToLink(isGeneratingCode: false)
} else {
state.mode = .notSupported
}
}
private func linkMobileDevice() {
state.mode = .readyToLink(isGeneratingCode: true)
// TODO: Generate a QR code.
actionsSubject.send(.linkMobileDevice)
}
}
@@ -0,0 +1,14 @@
//
// 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 Combine
@MainActor
protocol LinkNewDeviceScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<LinkNewDeviceScreenViewModelAction, Never> { get }
var context: LinkNewDeviceScreenViewModelType.Context { get }
}
@@ -0,0 +1,163 @@
//
// 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 LinkNewDeviceScreen: View {
@Bindable var context: LinkNewDeviceScreenViewModel.Context
var body: some View {
FullscreenDialog(topPadding: 24) {
mainContent
} bottomContent: {
buttons
}
.background()
.backgroundStyle(.compound.bgSubtleSecondary)
.navigationTitle(L10n.commonLinkNewDevice)
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
var mainContent: some View {
switch context.viewState.mode {
case .loading, .readyToLink:
VStack(spacing: 16) {
BigIcon(icon: \.computer, style: .default)
Text(L10n.screenLinkNewDeviceRootTitle)
.font(.compound.headingMDBold)
.foregroundColor(.compound.textPrimary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 8)
case .notSupported:
VStack(spacing: 16) {
BigIcon(icon: \.errorSolid, style: .alertSolid)
VStack(spacing: 8) {
Text(L10n.screenLinkNewDeviceErrorNotSupportedTitle)
.font(.compound.headingMDBold)
.foregroundColor(.compound.textPrimary)
.multilineTextAlignment(.center)
Text(L10n.screenLinkNewDeviceErrorNotSupportedSubtitle)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
.multilineTextAlignment(.center)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 40)
}
}
@ViewBuilder
var buttons: some View {
switch context.viewState.mode {
case .loading:
Button { } label: {
Label {
Text(L10n.commonLoading)
} icon: {
ProgressView()
.tint(.compound.iconOnSolidPrimary)
}
}
.buttonStyle(.compound(.primary))
.disabled(true)
case .readyToLink(let isGeneratingCode):
VStack(spacing: 16) {
Button { context.send(viewAction: .linkMobileDevice) } label: {
Label {
Text(isGeneratingCode ? L10n.screenLinkNewDeviceRootLoadingQrCode : L10n.screenLinkNewDeviceRootMobileDevice)
} icon: {
if isGeneratingCode {
ProgressView()
.tint(.compound.iconOnSolidPrimary)
} else {
CompoundIcon(\.mobile)
}
}
}
.buttonStyle(.compound(.primary))
Button { context.send(viewAction: .linkDesktopComputer) } label: {
Label(L10n.screenLinkNewDeviceRootDesktopComputer, icon: \.computer)
}
.buttonStyle(.compound(.primary))
}
.disabled(isGeneratingCode)
case .notSupported:
Button(L10n.actionDismiss) {
context.send(viewAction: .dismiss)
}
.buttonStyle(.compound(.primary))
}
}
}
// MARK: - Previews
struct LinkNewDeviceScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: false))
static let generatingViewModel = makeViewModel(mode: .readyToLink(isGeneratingCode: true))
static let loadingViewModel = makeViewModel(mode: .loading)
static let unsupportedViewModel = makeViewModel(mode: .notSupported)
static var previews: some View {
NavigationStack {
LinkNewDeviceScreen(context: viewModel.context)
}
.previewDisplayName("Ready")
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(isGeneratingCode: false) })
NavigationStack {
LinkNewDeviceScreen(context: generatingViewModel.context)
}
.previewDisplayName("Generating")
.snapshotPreferences(expect: generatingViewModel.context.observe(\.viewState.mode).map { $0 == .readyToLink(isGeneratingCode: true) })
NavigationStack {
LinkNewDeviceScreen(context: loadingViewModel.context)
}
.previewDisplayName("Loading")
NavigationStack {
LinkNewDeviceScreen(context: unsupportedViewModel.context)
}
.previewDisplayName("Unsupported")
.snapshotPreferences(expect: unsupportedViewModel.context.observe(\.viewState.mode).map { $0 == .notSupported })
}
static func makeViewModel(mode: LinkNewDeviceScreenViewState.Mode) -> LinkNewDeviceScreenViewModel {
let clientProxy = ClientProxyMock(.init())
clientProxy.isLoginWithQRCodeSupportedClosure = {
switch mode {
case .loading:
try? await Task.sleep(for: .seconds(20))
return false
case .readyToLink:
return true
case .notSupported:
return false
}
}
let viewModel = LinkNewDeviceScreenViewModel(clientProxy: clientProxy)
Task {
try? await Task.sleep(for: .milliseconds(100))
if case .readyToLink(isGeneratingCode: true) = mode {
viewModel.context.send(viewAction: .linkMobileDevice)
}
}
return viewModel
}
}
@@ -57,6 +57,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var linkPreviewsEnabled: Bool { get set }
var spaceSettingsEnabled: Bool { get set }
var linkNewDeviceEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -34,6 +34,9 @@ struct DeveloperOptionsScreen: View {
}
Section("General") {
Toggle(isOn: $context.linkNewDeviceEnabled) {
Text("Link new device with QR code")
}
Toggle(isOn: $context.spaceSettingsEnabled) {
Text("Space settings")
}
@@ -25,6 +25,7 @@ enum SettingsScreenCoordinatorAction {
case bugReport
case about
case blockedUsers
case linkNewDevice
case manageAccount(url: URL)
case notifications
case advancedSettings
@@ -59,6 +60,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.dismiss)
case .userDetails:
actionsSubject.send(.userDetails)
case .linkNewDevice:
actionsSubject.send(.linkNewDevice)
case let .manageAccount(url):
actionsSubject.send(.manageAccount(url: url))
case .analytics:
@@ -12,6 +12,7 @@ import UIKit
enum SettingsScreenViewModelAction: Equatable {
case close
case userDetails
case linkNewDevice
case manageAccount(url: URL)
case analytics
case appLock
@@ -35,6 +36,7 @@ enum SettingsScreenSecuritySectionMode {
struct SettingsScreenViewState: BindableState {
var deviceID: String?
var userID: String
var showLinkNewDeviceButton: Bool
var accountProfileURL: URL?
var accountSessionsListURL: URL?
var showAccountDeactivation: Bool
@@ -66,6 +68,7 @@ enum SettingsScreenViewAction {
case about
case blockedUsers
case secureBackup
case linkNewDevice
case manageAccount(url: URL)
case notifications
case enableDeveloperOptions
@@ -25,6 +25,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
userID: userSession.clientProxy.userID,
showLinkNewDeviceButton: appSettings.linkNewDeviceEnabled,
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
showDeveloperOptions: appSettings.developerOptionsEnabled,
showAnalyticsSettings: appSettings.canPromptForAnalytics,
@@ -35,6 +36,10 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
.weakAssign(to: \.state.showDeveloperOptions, on: self)
.store(in: &cancellables)
appSettings.$linkNewDeviceEnabled
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
.store(in: &cancellables)
userSession.clientProxy.userAvatarURLPublisher
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.userAvatarURL, on: self)
@@ -93,6 +98,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
actionsSubject.send(.close)
case .userDetails:
actionsSubject.send(.userDetails)
case .linkNewDevice:
actionsSubject.send(.linkNewDevice)
case let .manageAccount(url):
actionsSubject.send(.manageAccount(url: url))
case .analytics:
@@ -108,6 +108,14 @@ struct SettingsScreen: View {
private var manageAccountSection: some View {
Section {
if context.viewState.showLinkNewDeviceButton {
ListRow(label: .default(title: L10n.commonLinkNewDevice,
icon: \.devices),
kind: .navigationLink {
context.send(viewAction: .linkNewDevice)
})
}
if let url = context.viewState.accountProfileURL {
ListRow(label: .default(title: L10n.actionManageAccount,
icon: \.userProfile),
@@ -313,6 +313,17 @@ class ClientProxy: ClientProxyProtocol {
}
}
var isLoginWithQRCodeSupported: Bool {
get async {
do {
return try await client.isLoginWithQrCodeSupported()
} catch {
MXLog.error("Failed checking QR code support with error: \(error)")
return false
}
}
}
var maxMediaUploadSize: Result<UInt, ClientProxyError> {
get async {
do {
@@ -132,6 +132,8 @@ protocol ClientProxyProtocol: AnyObject {
var isLiveKitRTCSupported: Bool { get async }
var isLoginWithQRCodeSupported: Bool { get async }
var maxMediaUploadSize: Result<UInt, ClientProxyError> { get async }
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError>