From e75be71d9ac9153b0e6389e4bb16b41e58148b5c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 13 Mar 2024 01:25:02 +0700 Subject: [PATCH] android, desktop: migration enhancements (#3901) * adapted iOS logic * rename everything * title * title * text * rename * rename * alert on strange error --- .../chat/simplex/common/model/ChatModel.kt | 5 +- .../chat/simplex/common/model/SimpleXAPI.kt | 12 +- .../chat/simplex/common/platform/Core.kt | 4 + ...oAnotherDevice.kt => MigrateFromDevice.kt} | 264 ++++++++------- ...romAnotherDevice.kt => MigrateToDevice.kt} | 316 +++++++++--------- .../common/views/onboarding/SimpleXInfo.kt | 12 +- .../common/views/usersettings/SettingsView.kt | 5 +- .../commonMain/resources/MR/base/strings.xml | 113 ++++--- 8 files changed, 392 insertions(+), 339 deletions(-) rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/{MigrateToAnotherDevice.kt => MigrateFromDevice.kt} (64%) rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/{MigrateFromAnotherDevice.kt => MigrateToDevice.kt} (64%) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index faa4200555..a8a5797d71 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -13,7 +13,8 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.migration.MigrationFromAnotherDeviceState +import chat.simplex.common.views.migration.MigrationToDeviceState +import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -105,7 +106,7 @@ object ChatModel { // currently showing invitation val showingInvitation = mutableStateOf(null as ShowingInvitation?) - val migrationState: MutableState by lazy { mutableStateOf(MigrationFromAnotherDeviceState.transform()) } + val migrationState: MutableState by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) } var draft = mutableStateOf(null as ComposeState?) var draftChatId = mutableStateOf(null as String?) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 08d30fe086..85ce8dc00b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -147,7 +147,8 @@ class AppPreferences { val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } } - val migrationStage = mkStrPreference(SHARED_PREFS_MIGRATION_STAGE, null) + val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null) + val migrationFromStage = mkStrPreference(SHARED_PREFS_MIGRATION_FROM_STAGE, null) val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true) val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null) @@ -286,7 +287,8 @@ class AppPreferences { private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage" private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" - const val SHARED_PREFS_MIGRATION_STAGE = "MigrationStage" + const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage" + const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" @@ -704,9 +706,11 @@ object ChatController { throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}") } - suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): Boolean { + suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): CR.ChatCmdError? { val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl) - return r is CR.CmdOk + if (r is CR.CmdOk) return null + else if (r is CR.ChatCmdError) return r + throw Exception("failed to test storage encryption: ${r.responseType} ${r.details}") } suspend fun apiGetChats(rh: Long?): List { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 1a186ce8ad..f1a6d35e45 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -152,6 +152,10 @@ fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: fun chatInitControllerRemovingDatabases() { val dbPath = dbAbsolutePrefixPath + // Remove previous databases, otherwise, can be .errorNotADatabase with null controller + File(dbPath + "_chat.db").delete() + File(dbPath + "_agent.db").delete() + val dbKey = randomDatabasePassword() Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath") val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToAnotherDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt similarity index 64% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToAnotherDevice.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 5db6daf6de..da6e7181d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToAnotherDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -79,39 +79,49 @@ data class MigrationFileLinkData( @Serializable -private sealed class MigrationToState { - @Serializable object ChatStopInProgress: MigrationToState() - @Serializable data class ChatStopFailed(val reason: String): MigrationToState() - @Serializable object PassphraseNotSet: MigrationToState() - @Serializable object PassphraseConfirmation: MigrationToState() - @Serializable object UploadConfirmation: MigrationToState() - @Serializable object Archiving: MigrationToState() - @Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationToState() - @Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationToState() - @Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationToState() - @Serializable object LinkCreation: MigrationToState() - @Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationToState() - @Serializable data class Finished(val chatDeletion: Boolean): MigrationToState() +private sealed class MigrationFromState { + @Serializable object ChatStopInProgress: MigrationFromState() + @Serializable data class ChatStopFailed(val reason: String): MigrationFromState() + @Serializable object PassphraseNotSet: MigrationFromState() + @Serializable object PassphraseConfirmation: MigrationFromState() + @Serializable object UploadConfirmation: MigrationFromState() + @Serializable object Archiving: MigrationFromState() + @Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationFromState() + @Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationFromState() + @Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationFromState() + @Serializable object LinkCreation: MigrationFromState() + @Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationFromState() + @Serializable data class Finished(val chatDeletion: Boolean): MigrationFromState() } -private var MutableState.state: MigrationToState +private var MutableState.state: MigrationFromState get() = value set(v) { value = v } @Composable -fun MigrateToAnotherDeviceView(close: () -> Unit) { - val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(MigrationToState.ChatStopInProgress) } +fun MigrateFromDeviceView(close: () -> Unit) { + val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(MigrationFromState.ChatStopInProgress) } // Prevent from hiding the view until migration is finished or app deleted val backDisabled = remember { derivedStateOf { - migrationState.value is MigrationToState.DatabaseInit || - migrationState.value is MigrationToState.Archiving || - migrationState.value is MigrationToState.LinkCreation || - migrationState.value is MigrationToState.LinkShown || - migrationState.value is MigrationToState.Finished + when (migrationState.value) { + is MigrationFromState.ChatStopInProgress, + is MigrationFromState.DatabaseInit, + is MigrationFromState.Archiving, + is MigrationFromState.LinkShown, + is MigrationFromState.Finished -> true + + is MigrationFromState.ChatStopFailed, + is MigrationFromState.PassphraseNotSet, + is MigrationFromState.PassphraseConfirmation, + is MigrationFromState.UploadConfirmation, + is MigrationFromState.UploadProgress, + is MigrationFromState.UploadFailed, + is MigrationFromState.LinkCreation -> false + } } } - val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) } + val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) } ModalView( enableClose = !backDisabled.value, close = { @@ -121,7 +131,7 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) { close() }, ) { - MigrateToAnotherDeviceLayout( + MigrateFromDeviceLayout( migrationState = migrationState, chatReceiver = chatReceiver ) @@ -129,16 +139,16 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) { } @Composable -private fun MigrateToAnotherDeviceLayout( - migrationState: MutableState, - chatReceiver: MutableState +private fun MigrateFromDeviceLayout( + migrationState: MutableState, + chatReceiver: MutableState ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } Column( Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max), ) { - AppBarTitle(stringResource(MR.strings.migrate_to_device)) + AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() } @@ -147,30 +157,30 @@ private fun MigrateToAnotherDeviceLayout( @Composable private fun SectionByState( - migrationState: MutableState, + migrationState: MutableState, tempDatabaseFile: File, - chatReceiver: MutableState + chatReceiver: MutableState ) { when (val s = migrationState.value) { - is MigrationToState.ChatStopInProgress -> migrationState.ChatStopInProgressView() - is MigrationToState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason) - is MigrationToState.PassphraseNotSet -> migrationState.PassphraseNotSetView() - is MigrationToState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView() - is MigrationToState.UploadConfirmation -> migrationState.UploadConfirmationView() - is MigrationToState.Archiving -> migrationState.ArchivingView() - is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath) - is MigrationToState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath) - is MigrationToState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value) - is MigrationToState.LinkCreation -> LinkCreationView() - is MigrationToState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl) - is MigrationToState.Finished -> migrationState.FinishedView(s.chatDeletion) + is MigrationFromState.ChatStopInProgress -> migrationState.ChatStopInProgressView() + is MigrationFromState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason) + is MigrationFromState.PassphraseNotSet -> migrationState.PassphraseNotSetView() + is MigrationFromState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView() + is MigrationFromState.UploadConfirmation -> migrationState.UploadConfirmationView() + is MigrationFromState.Archiving -> migrationState.ArchivingView() + is MigrationFromState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath) + is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath) + is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value) + is MigrationFromState.LinkCreation -> LinkCreationView() + is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl) + is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion) } } @Composable -private fun MutableState.ChatStopInProgressView() { +private fun MutableState.ChatStopInProgressView() { Box { - SectionView(stringResource(MR.strings.migration_to_device_stopping_chat).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -179,7 +189,7 @@ private fun MutableState.ChatStopInProgressView() { } @Composable -private fun MutableState.ChatStopFailedView(reason: String) { +private fun MutableState.ChatStopFailedView(reason: String) { SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) { Text(reason) SectionSpacer() @@ -189,22 +199,22 @@ private fun MutableState.ChatStopFailedView(reason: String) { textColor = MaterialTheme.colors.error, click = ::stopChat ){} - SectionTextFooter(stringResource(MR.strings.migration_to_device_chat_should_be_stopped)) + SectionTextFooter(stringResource(MR.strings.migrate_from_device_chat_should_be_stopped)) } } @Composable -private fun MutableState.PassphraseNotSetView() { +private fun MutableState.PassphraseNotSetView() { DatabaseEncryptionView(chatModel, true) KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) { if (!appPreferences.initialRandomDBPassphrase.get()) { - state = MigrationToState.UploadConfirmation + state = MigrationFromState.UploadConfirmation } } } @Composable -private fun MutableState.PassphraseConfirmationView() { +private fun MutableState.PassphraseConfirmationView() { val useKeychain = remember { appPreferences.storeDBPassphrase.get() } val currentKey = rememberSaveable { mutableStateOf("") } val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } @@ -214,12 +224,12 @@ private fun MutableState.PassphraseConfirmationView() { ChatStoppedView() SectionSpacer() - SectionView(stringResource(MR.strings.migration_to_device_verify_database_passphrase).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) { PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true) SettingsActionItemWithContent( icon = painterResource(if (useKeychain) MR.images.ic_vpn_key_filled else MR.images.ic_lock), - text = stringResource(MR.strings.migration_to_device_verify_passphrase), + text = stringResource(MR.strings.migrate_from_device_verify_passphrase), textColor = MaterialTheme.colors.primary, disabled = verifyingPassphrase.value || currentKey.value.isEmpty(), click = { @@ -231,7 +241,7 @@ private fun MutableState.PassphraseConfirmationView() { } } ) {} - SectionTextFooter(stringResource(MR.strings.migration_to_device_confirm_you_remember_passphrase)) + SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase)) } } if (verifyingPassphrase.value) { @@ -241,22 +251,22 @@ private fun MutableState.PassphraseConfirmationView() { } @Composable -private fun MutableState.UploadConfirmationView() { - SectionView(stringResource(MR.strings.migration_to_device_confirm_upload).uppercase()) { +private fun MutableState.UploadConfirmationView() { + SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_ios_share), - text = stringResource(MR.strings.migration_to_device_archive_and_upload), + text = stringResource(MR.strings.migrate_from_device_archive_and_upload), textColor = MaterialTheme.colors.primary, - click = { state = MigrationToState.Archiving } + click = { state = MigrationFromState.Archiving } ){} - SectionTextFooter(stringResource(MR.strings.migration_to_device_all_data_will_be_uploaded)) + SectionTextFooter(stringResource(MR.strings.migrate_from_device_all_data_will_be_uploaded)) } } @Composable -private fun MutableState.ArchivingView() { +private fun MutableState.ArchivingView() { Box { - SectionView(stringResource(MR.strings.migration_to_device_archiving_database).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -265,9 +275,9 @@ private fun MutableState.ArchivingView() { } @Composable -private fun MutableState.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) { +private fun MutableState.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) { Box { - SectionView(stringResource(MR.strings.migration_to_device_database_init).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -276,19 +286,19 @@ private fun MutableState.DatabaseInitView(tempDatabaseFile: Fi } @Composable -private fun MutableState.UploadProgressView( +private fun MutableState.UploadProgressView( uploadedBytes: Long, totalBytes: Long, ctrl: ChatCtrl, user: User, tempDatabaseFile: File, - chatReceiver: MutableState, + chatReceiver: MutableState, archivePath: String, ) { Box { - SectionView(stringResource(MR.strings.migration_to_device_uploading_archive).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) { val ratio = uploadedBytes.toFloat() / max(totalBytes, 1) - LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_to_device_bytes_uploaded).format(formatBytes(uploadedBytes))) + LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes))) } } LaunchedEffect(Unit) { @@ -297,17 +307,17 @@ private fun MutableState.UploadProgressView( } @Composable -private fun MutableState.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationToChatReceiver?) { - SectionView(stringResource(MR.strings.migration_to_device_upload_failed).uppercase()) { +private fun MutableState.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) { + SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_ios_share), - text = stringResource(MR.strings.migration_to_device_repeat_upload), + text = stringResource(MR.strings.migrate_from_device_repeat_upload), textColor = MaterialTheme.colors.primary, click = { - state = MigrationToState.DatabaseInit(totalBytes, archivePath) + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) } ) {} - SectionTextFooter(stringResource(MR.strings.migration_to_device_try_again)) + SectionTextFooter(stringResource(MR.strings.migrate_from_device_try_again)) } LaunchedEffect(Unit) { chatReceiver?.stopAndCleanUp() @@ -317,17 +327,17 @@ private fun MutableState.UploadFailedView(totalBytes: Long, ar @Composable private fun LinkCreationView() { Box { - SectionView(stringResource(MR.strings.migration_to_device_creating_archive_link).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {} ProgressView() } } @Composable -private fun MutableState.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) { +private fun MutableState.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) { SectionView { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_close), - text = stringResource(MR.strings.migration_to_device_cancel_migration), + text = stringResource(MR.strings.migrate_from_device_cancel_migration), textColor = MaterialTheme.colors.error, click = { cancelMigration(fileId, ctrl) @@ -335,31 +345,32 @@ private fun MutableState.LinkShownView(fileId: Long, link: Str ) {} SettingsActionItemWithContent( icon = painterResource(MR.images.ic_check), - text = stringResource(MR.strings.migration_to_device_finalize_migration), + text = stringResource(MR.strings.migrate_from_device_finalize_migration), textColor = MaterialTheme.colors.primary, click = { finishMigration(fileId, ctrl) } ) {} - SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_choose_migrate_from_another_device)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device)) } SectionSpacer() SectionView(stringResource(MR.strings.show_QR_code).uppercase()) { SimpleXLinkQRCode(link, onShare = {}) } SectionSpacer() - SectionView(stringResource(MR.strings.migration_to_device_or_share_this_file_link).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) { LinkTextView(link, true) } } @Composable -private fun MutableState.FinishedView(chatDeletion: Boolean) { +private fun MutableState.FinishedView(chatDeletion: Boolean) { Box { - SectionView(stringResource(MR.strings.migration_to_device_migration_complete).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_delete_forever), - text = stringResource(MR.strings.migration_to_device_delete_database_from_device), + text = stringResource(MR.strings.migrate_from_device_delete_database_from_device), textColor = MaterialTheme.colors.primary, click = { AlertManager.shared.showAlertDialog( @@ -375,21 +386,21 @@ private fun MutableState.FinishedView(chatDeletion: Boolean) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_play_arrow_filled), - text = stringResource(MR.strings.migration_to_device_start_chat), + text = stringResource(MR.strings.migrate_from_device_start_chat), textColor = MaterialTheme.colors.error, click = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.start_chat_question), - text = generalGetString(MR.strings.migration_to_device_starting_chat_on_multiple_devices_unsupported), - confirmText = generalGetString(MR.strings.migration_to_device_start_chat), + text = generalGetString(MR.strings.migrate_from_device_starting_chat_on_multiple_devices_unsupported), + confirmText = generalGetString(MR.strings.migrate_from_device_start_chat), onConfirm = { withLongRunningApi { startChatAndDismiss() } } ) } ) {} - SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_you_must_not_start_database_on_two_device)) - SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_using_on_two_device_breaks_encryption)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) } if (chatDeletion) { ProgressView() @@ -420,52 +431,58 @@ fun LargeProgressView(value: Float, title: String, description: String) { } } -private fun MutableState.stopChat() { +private fun MutableState.stopChat() { withBGApi { try { stopChatAsync(chatModel) try { controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) - state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationToState.PassphraseNotSet else MigrationToState.PassphraseConfirmation + state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation } catch (e: Exception) { AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.migrate_to_device_error_saving_settings), + title = generalGetString(MR.strings.migrate_from_device_error_saving_settings), text = e.stackTraceToString() ) - state = MigrationToState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_to_device_error_saving_settings)) + state = MigrationFromState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_from_device_error_saving_settings)) } } catch (e: Exception) { - state = MigrationToState.ChatStopFailed(reason = e.stackTraceToString().take(10)) + state = MigrationFromState.ChatStopFailed(reason = e.stackTraceToString().take(10)) } } } -private suspend fun MutableState.verifyDatabasePassphrase(dbKey: String) { - if (controller.testStorageEncryption(dbKey)) { - state = MigrationToState.UploadConfirmation - } else { +private suspend fun MutableState.verifyDatabasePassphrase(dbKey: String) { + val error = controller.testStorageEncryption(dbKey) + if (error == null) { + state = MigrationFromState.UploadConfirmation + } else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) { showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase("")) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error), + text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details + ) } } -private fun MutableState.exportArchive() { +private fun MutableState.exportArchive() { withLongRunningApi { try { getMigrationTempFilesDirectory().mkdir() val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf("")) val totalBytes = File(archivePath).length() if (totalBytes > 0L) { - state = MigrationToState.DatabaseInit(totalBytes, archivePath) + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) } else { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_exported_file_doesnt_exist)) - state = MigrationToState.UploadConfirmation + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) + state = MigrationFromState.UploadConfirmation } } catch (e: Exception) { AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.migrate_to_device_error_exporting_archive), + title = generalGetString(MR.strings.migrate_from_device_error_exporting_archive), text = e.stackTraceToString() ) - state = MigrationToState.UploadConfirmation + state = MigrationFromState.UploadConfirmation } } } @@ -484,7 +501,7 @@ suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair< return null } -private fun MutableState.prepareDatabase( +private fun MutableState.prepareDatabase( tempDatabaseFile: File, totalBytes: Long, archivePath: String, @@ -492,35 +509,35 @@ private fun MutableState.prepareDatabase( withLongRunningApi { val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg()) if (ctrlAndUser == null) { - state = MigrationToState.UploadFailed(totalBytes, archivePath) + state = MigrationFromState.UploadFailed(totalBytes, archivePath) return@withLongRunningApi } val (ctrl, user) = ctrlAndUser - state = MigrationToState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user) + state = MigrationFromState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user) } } -private fun MutableState.startUploading( +private fun MutableState.startUploading( totalBytes: Long, ctrl: ChatCtrl, user: User, tempDatabaseFile: File, - chatReceiver: MutableState, + chatReceiver: MutableState, archivePath: String, ) { withBGApi { - chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg -> + chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg -> when (msg) { is CR.SndFileProgressXFTP -> { val s = state - if (s is MigrationToState.UploadProgress && s.uploadedBytes != s.totalBytes) { - state = MigrationToState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user) + if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) { + state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user) } } is CR.SndFileRedirectStartXFTP -> { delay(500) - state = MigrationToState.LinkCreation + state = MigrationFromState.LinkCreation } is CR.SndStandaloneFileComplete -> { delay(500) @@ -532,7 +549,14 @@ private fun MutableState.startUploading( requiredHostMode = cfg.requiredHostMode ) ) - state = MigrationToState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl) + state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl) + } + is CR.SndFileError -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_from_device_upload_failed), + generalGetString(MR.strings.migrate_from_device_check_connection_and_try_again) + ) + state = MigrationFromState.UploadFailed(totalBytes, archivePath) } else -> { Log.d(TAG, "unsupported event: ${msg.responseType}") @@ -544,13 +568,13 @@ private fun MutableState.startUploading( val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl) if (res == null) { - state = MigrationToState.UploadFailed(totalBytes, archivePath) + state = MigrationFromState.UploadFailed(totalBytes, archivePath) return@withBGApi AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.migration_to_device_error_uploading_archive), + generalGetString(MR.strings.migrate_from_device_error_uploading_archive), error ) } - state = MigrationToState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user) + state = MigrationFromState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user) } } @@ -565,19 +589,19 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) { } } -private fun MutableState.finishMigration(fileId: Long, ctrl: ChatCtrl) { +private fun MutableState.finishMigration(fileId: Long, ctrl: ChatCtrl) { withBGApi { cancelUploadedArchive(fileId, ctrl) - state = MigrationToState.Finished(false) + state = MigrationFromState.Finished(false) } } -private fun MutableState.deleteChatAndDismiss() { +private fun MutableState.deleteChatAndDismiss() { withBGApi { try { deleteChatAsync(chatModel) chatModel.chatDbChanged.value = true - state = MigrationToState.Finished(true) + state = MigrationFromState.Finished(true) try { initChatController(startChat = { CompletableDeferred(false) }) chatModel.chatDbChanged.value = false @@ -587,7 +611,7 @@ private fun MutableState.deleteChatAndDismiss() { } } catch (e: Exception) { AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.migration_to_device_error_deleting_database), + title = generalGetString(MR.strings.migrate_from_device_error_deleting_database), text = e.stackTraceToString() ) } @@ -615,14 +639,14 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) { } } -private suspend fun MutableState.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) { +private suspend fun MutableState.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) { val s = state - if (s !is MigrationToState.LinkShown && s !is MigrationToState.Finished) { + if (s !is MigrationFromState.LinkShown && s !is MigrationFromState.Finished) { chatModel.switchingUsersAndHosts.value = true startChatAndDismiss(false) chatModel.switchingUsersAndHosts.value = false } - if (s is MigrationToState.UploadProgress) { + if (s is MigrationFromState.UploadProgress) { cancelUploadedArchive(s.fileId, s.ctrl) } chatReceiver?.stopAndCleanUp() @@ -632,7 +656,7 @@ private suspend fun MutableState.cleanUpOnBack(chatReceiver: M private fun fileForTemporaryDatabase(): File = File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory())) -private class MigrationToChatReceiver( +private class MigrationFromChatReceiver( val ctrl: ChatCtrl, val databaseUrl: File, var receiveMessages: Boolean = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromAnotherDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt similarity index 64% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromAnotherDevice.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 52698a6e47..e24570ba8b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromAnotherDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -13,7 +13,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import chat.simplex.common.model.* -import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_STAGE +import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.startChat import chat.simplex.common.model.ChatCtrl @@ -39,100 +39,97 @@ import java.util.* import kotlin.math.max @Serializable -sealed class MigrationFromAnotherDeviceState { - @Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationFromAnotherDeviceState() - @Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState() - @Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState() - @Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationFromAnotherDeviceState() +sealed class MigrationToDeviceState { + @Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState() + @Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState() + @Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState() + @Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState() companion object { // Here we check whether it's needed to show migration process after app restart or not // It's important to NOT show the process when archive was corrupted/not fully downloaded - fun transform(): MigrationFromAnotherDeviceState? { - val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_STAGE) - var state: MigrationFromAnotherDeviceState? = if (stage != null) json.decodeFromString(stage) else null - if (state is DownloadProgress) { - // No migration happens at the moment actually since archive were not downloaded fully - Log.e(TAG, "MigrateFromDevice: archive wasn't fully downloaded, removed broken file") - state = null - } else if (state is Onion) { - state = null - } else if (state is ArchiveImport && !File(getMigrationTempFilesDirectory(), state.archiveName).exists()) { - Log.e(TAG, "MigrateFromDevice: archive was removed unintentionally or state is broken, dropping migration") - state = null + fun makeMigrationState(): MigrationToState? { + val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_TO_STAGE) + val state: MigrationToDeviceState? = if (stage != null) json.decodeFromString(stage) else null + val initial: MigrationToState? = when(state) { + null -> null + is DownloadProgress -> { + // No migration happens at the moment actually since archive were not downloaded fully + Log.e(TAG, "MigrateToDevice: archive wasn't fully downloaded, removed broken file") + null + } + is Onion -> null + is ArchiveImport -> { + if (!File(getMigrationTempFilesDirectory(), state.archiveName).exists()) { + Log.e(TAG, "MigrateToDevice: archive was removed unintentionally or state is broken, dropping migration") + null + } else { + val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName) + MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg) + } + } + is Passphrase -> MigrationToState.Passphrase("", state.netCfg) } - if (state == null) { - settings.remove(SHARED_PREFS_MIGRATION_STAGE) + if (initial == null) { + settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE) getMigrationTempFilesDirectory().deleteRecursively() } - return state + return initial } - fun save(state: MigrationFromAnotherDeviceState?) { + fun save(state: MigrationToDeviceState?) { if (state != null) { - appPreferences.migrationStage.set(json.encodeToString(state)) + appPreferences.migrationToStage.set(json.encodeToString(state)) } else { - appPreferences.migrationStage.set(null) + appPreferences.migrationToStage.set(null) } - chatModel.migrationState.value = state } } } @Serializable -private sealed class MigrationState { - @Serializable object PasteOrScanLink: MigrationState() - @Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationState() - @Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationState() - @Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationState() - @Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationState() - @Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationState() - @Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationState() - @Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationState() - @Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationState() - @Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState() - @Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState() +sealed class MigrationToState { + @Serializable object PasteOrScanLink: MigrationToState() + @Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState() + @Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState() + @Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState() + @Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState() + @Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState() } -private var MutableState.state: MigrationState +private var MutableState.state: MigrationToState? get() = value set(v) { value = v } @Composable -fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceState? = null, close: () -> Unit) { - val migrationState = rememberSaveable(stateSaver = serializableSaver()) { - mutableStateOf( - when (state) { - null -> MigrationState.PasteOrScanLink - is MigrationFromAnotherDeviceState.Onion -> { - MigrationState.Onion(state.link, state.socksProxy, state.hostMode, state.requiredHostMode) - } - is MigrationFromAnotherDeviceState.DownloadProgress -> { - val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName) - // SHOULDN'T BE HERE because the app checks this before opening migration screen and will not open it in this case. - // See analyzeMigrationState() - MigrationState.DownloadFailed(totalBytes = 0, link = state.link, archivePath = archivePath.absolutePath, state.netCfg) - } - is MigrationFromAnotherDeviceState.ArchiveImport -> { - val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName) - MigrationState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg) - } - is MigrationFromAnotherDeviceState.Passphrase -> { - MigrationState.Passphrase("", state.netCfg) - } - } - ) - } +fun ModalData.MigrateToDeviceView(close: () -> Unit) { + val migrationState = remember { chatModel.migrationState } // Prevent from hiding the view until migration is finished or app deleted val backDisabled = remember { derivedStateOf { - val s = chatModel.migrationState.value - s is MigrationFromAnotherDeviceState.ArchiveImport || - s is MigrationFromAnotherDeviceState.Passphrase || - migrationState.value is MigrationState.DatabaseInit + when (chatModel.migrationState.value) { + null, + is MigrationToState.PasteOrScanLink, + is MigrationToState.Onion, + is MigrationToState.LinkDownloading, + is MigrationToState.DownloadProgress, + is MigrationToState.DownloadFailed, + is MigrationToState.ArchiveImportFailed -> false + + is MigrationToState.ArchiveImport, + is MigrationToState.DatabaseInit, + is MigrationToState.Migration, + is MigrationToState.MigrationConfirmation, + is MigrationToState.Passphrase -> true + } } } - val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) } + val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) } ModalView( enableClose = !backDisabled.value, close = { @@ -142,7 +139,7 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat } }, ) { - MigrateFromAnotherDeviceLayout( + MigrateToDeviceLayout( migrationState = migrationState, chatReceiver = chatReceiver, close = close, @@ -151,9 +148,9 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat } @Composable -private fun ModalData.MigrateFromAnotherDeviceLayout( - migrationState: MutableState, - chatReceiver: MutableState, +private fun ModalData.MigrateToDeviceLayout( + migrationState: MutableState, + chatReceiver: MutableState, close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } @@ -161,7 +158,7 @@ private fun ModalData.MigrateFromAnotherDeviceLayout( Column( Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max), ) { - AppBarTitle(stringResource(MR.strings.migrate_here)) + AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() } @@ -170,28 +167,29 @@ private fun ModalData.MigrateFromAnotherDeviceLayout( @Composable private fun ModalData.SectionByState( - migrationState: MutableState, + migrationState: MutableState, tempDatabaseFile: File, - chatReceiver: MutableState, + chatReceiver: MutableState, close: () -> Unit ) { when (val s = migrationState.value) { - is MigrationState.PasteOrScanLink -> migrationState.PasteOrScanLinkView() - is MigrationState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState) - is MigrationState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg) - is MigrationState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg) - is MigrationState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes) - is MigrationState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg) - is MigrationState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg) - is MigrationState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg) - is MigrationState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg) - is MigrationState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg) - is MigrationState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close) + null -> {} + is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView() + is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState) + is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg) + is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg) + is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes) + is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg) + is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg) + is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg) + is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg) + is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg) + is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close) } } @Composable -private fun MutableState.PasteOrScanLinkView() { +private fun MutableState.PasteOrScanLinkView() { if (appPlatform.isAndroid) { SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> @@ -209,7 +207,7 @@ private fun MutableState.PasteOrScanLinkView() { } @Composable -private fun MutableState.PasteLinkView() { +private fun MutableState.PasteLinkView() { val clipboard = LocalClipboardManager.current SectionItemView({ val str = clipboard.getText()?.text ?: return@SectionItemView @@ -220,7 +218,7 @@ private fun MutableState.PasteLinkView() { } @Composable -private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { +private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { val onionHosts = remember { stateGetOrPut("onionHosts") { getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts } } @@ -238,10 +236,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value)) } - SectionView(stringResource(MR.strings.migration_from_device_confirm_network_settings).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_check), - text = stringResource(MR.strings.migration_from_device_apply_onion), + text = stringResource(MR.strings.migrate_to_device_apply_onion), textColor = MaterialTheme.colors.primary, click = { val updated = netCfg.value @@ -251,11 +249,11 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos sessionMode = sessionMode.value ) withBGApi { - state.value = MigrationState.DatabaseInit(link, updated) + state.value = MigrationToState.DatabaseInit(link, updated) } } ){} - SectionTextFooter(stringResource(MR.strings.migration_from_device_confirm_network_settings_footer)) + SectionTextFooter(stringResource(MR.strings.migrate_to_device_confirm_network_settings_footer)) } SectionSpacer() @@ -285,9 +283,9 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos } @Composable -private fun MutableState.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) { +private fun MutableState.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) { Box { - SectionView(stringResource(MR.strings.migration_from_device_database_init).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -296,17 +294,17 @@ private fun MutableState.DatabaseInitView(link: String, tempData } @Composable -private fun MutableState.LinkDownloadingView( +private fun MutableState.LinkDownloadingView( link: String, ctrl: ChatCtrl, user: User, archivePath: String, tempDatabaseFile: File, - chatReceiver: MutableState, + chatReceiver: MutableState, netCfg: NetCfg ) { Box { - SectionView(stringResource(MR.strings.migration_from_device_downloading_details).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -317,37 +315,37 @@ private fun MutableState.LinkDownloadingView( @Composable private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { Box { - SectionView(stringResource(MR.strings.migration_from_device_downloading_archive).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) { val ratio = downloadedBytes.toFloat() / max(totalBytes, 1) - LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_from_device_bytes_downloaded).format(formatBytes(downloadedBytes))) + LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes))) } } } @Composable -private fun MutableState.DownloadFailedView(link: String, chatReceiver: MigrationFromChatReceiver?, archivePath: String, netCfg: NetCfg) { - SectionView(stringResource(MR.strings.migration_from_device_download_failed).uppercase()) { +private fun MutableState.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) { + SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), - text = stringResource(MR.strings.migration_from_device_repeat_download), + text = stringResource(MR.strings.migrate_to_device_repeat_download), textColor = MaterialTheme.colors.primary, click = { - state = MigrationState.DatabaseInit(link, netCfg) + state = MigrationToState.DatabaseInit(link, netCfg) } ) {} - SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again)) + SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) } LaunchedEffect(Unit) { chatReceiver?.stopAndCleanUp() File(archivePath).delete() - MigrationFromAnotherDeviceState.save(null) + MigrationToDeviceState.save(null) } } @Composable -private fun MutableState.ArchiveImportView(archivePath: String, netCfg: NetCfg) { +private fun MutableState.ArchiveImportView(archivePath: String, netCfg: NetCfg) { Box { - SectionView(stringResource(MR.strings.migration_from_device_importing_archive).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -356,29 +354,29 @@ private fun MutableState.ArchiveImportView(archivePath: String, } @Composable -private fun MutableState.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) { - SectionView(stringResource(MR.strings.migration_from_device_import_failed).uppercase()) { +private fun MutableState.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) { + SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), - text = stringResource(MR.strings.migration_from_device_repeat_import), + text = stringResource(MR.strings.migrate_to_device_repeat_import), textColor = MaterialTheme.colors.primary, click = { - state = MigrationState.ArchiveImport(archivePath, netCfg) + state = MigrationToState.ArchiveImport(archivePath, netCfg) } ) {} - SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again)) + SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) } } @Composable -private fun MutableState.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) { +private fun MutableState.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) { val currentKey = rememberSaveable { mutableStateOf(currentKey) } val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) } Box { val view = LocalMultiplatformView() - SectionView(stringResource(MR.strings.migration_from_device_enter_passphrase).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) { SavePassphraseSetting( useKeychain.value, false, @@ -401,9 +399,9 @@ private fun MutableState.PassphraseEnteringView(currentKey: Stri val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp) val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation if (success) { - state = MigrationState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg) + state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg) } else if (status is DBMigrationResult.ErrorMigration) { - state = MigrationState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg) + state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg) } else { showErrorOnMigrationIfNeeded(status) } @@ -420,7 +418,7 @@ private fun MutableState.PassphraseEnteringView(currentKey: Stri } @Composable -private fun MutableState.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) { +private fun MutableState.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) { data class Tuple4(val a: A, val b: B, val c: C, val d: D) val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) { is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { @@ -455,7 +453,7 @@ private fun MutableState.MigrationConfirmationView(status: DBMig text = button, textColor = MaterialTheme.colors.primary, click = { - state = MigrationState.Migration(passphrase, confirmation, useKeychain, netCfg) + state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg) } ) {} } @@ -466,7 +464,7 @@ private fun MutableState.MigrationConfirmationView(status: DBMig @Composable private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) { Box { - SectionView(stringResource(MR.strings.migration_from_device_migrating).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { @@ -479,18 +477,18 @@ private fun ProgressView() { DefaultProgressView(null) } -private suspend fun MutableState.checkUserLink(link: String) { +private suspend fun MutableState.checkUserLink(link: String) { if (strHasSimplexFileLink(link.trim())) { val data = MigrationFileLinkData.readFromLink(link) val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false val networkConfig = data?.networkConfig?.transformToPlatformSupported() // If any of iOS or Android had onion enabled, show onion screen if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) { - state = MigrationState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode) - MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) + state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode) + MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) } else { val current = getNetCfg() - state = MigrationState.DatabaseInit(link.trim(), current.copy( + state = MigrationToState.DatabaseInit(link.trim(), current.copy( socksProxy = networkConfig?.socksProxy, hostMode = networkConfig?.hostMode ?: current.hostMode, requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode @@ -504,7 +502,7 @@ private suspend fun MutableState.checkUserLink(link: String) { } } -private fun MutableState.prepareDatabase( +private fun MutableState.prepareDatabase( link: String, tempDatabaseFile: File, netCfg: NetCfg, @@ -512,43 +510,59 @@ private fun MutableState.prepareDatabase( withLongRunningApi { val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg) if (ctrlAndUser == null) { - state = MigrationState.DownloadFailed(0, link, archivePath(), netCfg) + state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg) return@withLongRunningApi } val (ctrl, user) = ctrlAndUser - state = MigrationState.LinkDownloading(link, ctrl, user, archivePath(), netCfg) + state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg) } } -private fun MutableState.startDownloading( +private fun MutableState.startDownloading( totalBytes: Long, ctrl: ChatCtrl, user: User, tempDatabaseFile: File, - chatReceiver: MutableState, + chatReceiver: MutableState, link: String, archivePath: String, netCfg: NetCfg, ) { withBGApi { - chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg -> + chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg -> when (msg) { is CR.RcvFileProgressXFTP -> { - state = MigrationState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl) - MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.DownloadProgress(link, File(archivePath).name, netCfg)) + state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl) + MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg)) } is CR.RcvStandaloneFileComplete -> { delay(500) - state = MigrationState.ArchiveImport(archivePath, netCfg) - MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.ArchiveImport(File(archivePath).name, netCfg)) + // User closed the whole screen before new state was saved + if (state == null) { + MigrationToDeviceState.save(null) + } else { + state = MigrationToState.ArchiveImport(archivePath, netCfg) + MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg)) + } } is CR.RcvFileError -> { AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.migration_from_device_download_failed), - generalGetString(MR.strings.migration_from_device_file_delete_or_link_invalid) + generalGetString(MR.strings.migrate_to_device_download_failed), + generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) ) - state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg) + } + is CR.ChatRespError -> { + if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_to_device_download_failed), + generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) + ) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg) + } else { + Log.d(TAG, "unsupported error: ${msg.responseType}") + } } else -> Log.d(TAG, "unsupported event: ${msg.responseType}") } @@ -557,16 +571,16 @@ private fun MutableState.startDownloading( val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl) if (res == null) { - state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg) AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.migration_from_device_error_downloading_archive), + generalGetString(MR.strings.migrate_to_device_error_downloading_archive), error ) } } } -private fun MutableState.importArchive(archivePath: String, netCfg: NetCfg) { +private fun MutableState.importArchive(archivePath: String, netCfg: NetCfg) { withLongRunningApi { try { if (ChatController.ctrl == null || ChatController.ctrl == -1L) { @@ -582,14 +596,14 @@ private fun MutableState.importArchive(archivePath: String, netC generalGetString(MR.strings.non_fatal_errors_occured_during_import) ) } - state = MigrationState.Passphrase("", netCfg) - MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Passphrase(netCfg)) + state = MigrationToState.Passphrase("", netCfg) + MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg)) } catch (e: Exception) { - state = MigrationState.ArchiveImportFailed(archivePath, netCfg) + state = MigrationToState.ArchiveImportFailed(archivePath, netCfg) AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString()) } } catch (e: Exception) { - state = MigrationState.ArchiveImportFailed(archivePath, netCfg) + state = MigrationToState.ArchiveImportFailed(archivePath, netCfg) AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString()) } } @@ -630,30 +644,32 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit) startChat(user) } hideView(close) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migration_from_device_chat_migrated), generalGetString(MR.strings.migration_from_device_finalize_migration)) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration)) } catch (e: Exception) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString()) } - MigrationFromAnotherDeviceState.save(null) + MigrationToDeviceState.save(null) } private fun hideView(close: () -> Unit) { appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete) + chatModel.migrationState.value = null close() } -private suspend fun MutableState.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) { +private suspend fun MutableState.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) { val state = state - if (state is MigrationState.ArchiveImportFailed) { + if (state is MigrationToState.ArchiveImportFailed) { // Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state deleteChatDatabaseFilesAndState() initChatControllerAndRunMigrations() - } else if (state is MigrationState.DownloadProgress && state.ctrl != null) { + } else if (state is MigrationToState.DownloadProgress && state.ctrl != null) { stopArchiveDownloading(state.fileId, state.ctrl) } chatReceiver?.stopAndCleanUp() getMigrationTempFilesDirectory().deleteRecursively() - MigrationFromAnotherDeviceState.save(null) + MigrationToDeviceState.save(null) + chatModel.migrationState.value = null } private fun strHasSimplexFileLink(text: String): Boolean = @@ -670,7 +686,7 @@ private fun archivePath(): String { return archivePath.absolutePath } -private class MigrationFromChatReceiver( +private class MigrationToChatReceiver( val ctrl: ChatCtrl, val databaseUrl: File, var receiveMessages: Boolean = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index 0fe756d8fb..b82852664b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -19,7 +19,8 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView +import chat.simplex.common.views.migration.MigrateToDeviceView +import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource @@ -71,7 +72,9 @@ fun SimpleXInfoLayout( .padding(top = DEFAULT_PADDING), contentAlignment = Alignment.Center ) { SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download), - click = { ModalManager.fullscreen.showCustomModal { close -> MigrateFromAnotherDeviceView(chatModel.migrationState.value, close) } }) + click = { + chatModel.migrationState.value = MigrationToState.PasteOrScanLink + ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } }) } } @@ -85,9 +88,8 @@ fun SimpleXInfoLayout( } } LaunchedEffect(Unit) { - val state = chatModel.migrationState.value - if (state != null && !ModalManager.fullscreen.hasModalsOpen()) { - ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateFromAnotherDeviceView(state, close) } + if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) { + ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index ffa908e210..3ef02f536a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -28,8 +28,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.CreateProfile import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView -import chat.simplex.common.views.migration.MigrateToAnotherDeviceView +import chat.simplex.common.views.migration.MigrateFromDeviceView import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.WhatsNewView import chat.simplex.common.views.remote.ConnectDesktopView @@ -137,7 +136,7 @@ fun SettingsLayout( } else { SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) } - SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_to_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateToAnotherDeviceView(close) } }}, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true) } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index f3b7bf4a72..fca2adfafb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1846,63 +1846,66 @@ Please report it to the developers: \n%s Restart chat - - - Migrate here + + Migrate here Or paste archive link Paste archive link Invalid link - Migrating - Preparing download - Downloading link details - Downloading archive - %s downloaded - Download failed - Repeat download - You can give another try. - Importing archive - Import failed - Repeat import - Enter passphrase - File was deleted or link is invalid - Error downloading the archive - Chat migrated! - Finalize migration on another device. - Confirm network settings - Please confirm that network settings are correct for this device. - Apply + Migrating + Preparing download + Downloading link details + Downloading archive + %s downloaded + Download failed + Repeat download + You can give another try. + Importing archive + Import failed + Repeat import + Enter passphrase + File was deleted or link is invalid + Error downloading the archive + Chat migrated! + Finalize migration on another device. + Confirm network settings + Please confirm that network settings are correct for this device. + Apply - - Migrate to another device - Error saving settings - Exported file doesn\'t exist - Error exporting chat database - Preparing upload - Error uploading the archive - Error deleting database - Stopping chat - In order to continue, chat should be stopped. - Archive and upload - Confirm upload - All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Archiving database - %s uploaded - Uploading archive - Upload failed - Repeat upload - You can give another try. - Creating archive link - Cancel migration - Finalize migration - Migrate from another device on the new device and scan QR code.]]> - Or securely share this file link - Delete database from this device - Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Start chat - Migration complete - must not use the same database on two devices.]]> - Please note: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]> - Verify database passphrase - Verify passphrase - Confirm that you remember database passphrase to migrate it. + + Migrate device + Migrate to another device + Error saving settings + Exported file doesn\'t exist + Error exporting chat database + Preparing upload + Error uploading the archive + Error deleting database + Stopping chat + In order to continue, chat should be stopped. + Archive and upload + Confirm upload + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + Archiving database + %s uploaded + Uploading archive + Upload failed + Repeat upload + You can give another try. + Creating archive link + Cancel migration + Finalize migration + Migrate from another device on the new device and scan QR code.]]> + Or securely share this file link + Delete database from this device + Warning: starting chat on multiple devices is not supported and will cause message delivery failures + Start chat + Migration complete + must not use the same database on two devices.]]> + Please note: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]> + Verify database passphrase + Verify passphrase + Confirm that you remember database passphrase to migrate it. + Check your internet connection and try again + Warning: the archive will be deleted.]]> + Error verifying passphrase: \ No newline at end of file