From 31bb744ba7529defd83d617023e326917e57fc9d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 27 Mar 2023 23:16:07 +0100 Subject: [PATCH 1/4] android: support down migrations --- apps/android/app/src/main/cpp/simplex-api.c | 8 +- .../main/java/chat/simplex/app/SimplexApp.kt | 7 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 8 +- .../app/views/database/DatabaseErrorView.kt | 133 ++++++++++++------ .../app/views/helpers/DatabaseUtils.kt | 33 ++++- .../app/views/usersettings/DeveloperView.kt | 47 +++++++ .../app/views/usersettings/SettingsView.kt | 19 +-- .../app/src/main/res/values/strings.xml | 5 + 8 files changed, 189 insertions(+), 71 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index edd07bd63b..9a3c9c48ab 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -35,7 +35,7 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass // from simplex-chat typedef long* chat_ctrl; -extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl); +extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); @@ -44,13 +44,15 @@ extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); JNIEXPORT jobjectArray JNICALL -Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) { +Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); + const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE); jlong _ctrl = (jlong) 0; - jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl)); + jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); + (*env)->ReleaseStringUTFChars(env, dbKey, _confirm); // Creating array of Object's (boxed values can be passed, eg. Long instead of long) jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index dce371a351..1559002080 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long -external fun chatMigrateInit(dbPath: String, dbKey: String): Array +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String @@ -41,10 +41,11 @@ class SimplexApp: Application(), LifecycleEventObserver { val defaultLocale: Locale = Locale.getDefault() - fun initChatController(useKey: String? = null, startChat: Boolean = true) { + fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context) - val migrated: Array = chatMigrateInit(dbAbsolutePathPrefix, dbKey) + val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + val migrated: Array = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value) val res: DBMigrationResult = kotlin.runCatching { json.decodeFromString(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 2b909e6991..56794e41b2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -142,6 +142,7 @@ class AppPreferences(val context: Context) { val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null) val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null) val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true) + val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb()) @@ -242,6 +243,7 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" + private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" @@ -3024,7 +3026,7 @@ sealed class CR { @Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR() @Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR() @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR() - @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): CR() + @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List, val agentMigrations: List): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() @Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR() @@ -3228,7 +3230,9 @@ sealed class CR { is CallEnded -> withUser(user, "contact: ${contact.id}") is NewContactConnection -> withUser(user, json.encodeToString(connection)) is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) - is VersionInfo -> json.encodeToString(versionInfo) + is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + + "agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}" is CmdOk -> withUser(user, noDetails()) is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 9f69ca2799..623e6049f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -4,6 +4,7 @@ import SectionSpacer import SectionView import android.content.Context import android.util.Log +import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -47,33 +48,42 @@ fun DatabaseErrorView( appPreferences.initialRandomDBPassphrase.set(false) runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } - val title = when (chatDbStatus.value) { - is DBMigrationResult.OK -> "" - is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty()) - generalGetString(R.string.wrong_passphrase) - else - generalGetString(R.string.encrypted_database) - is DBMigrationResult.Error -> generalGetString(R.string.database_error) - is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error) - is DBMigrationResult.Unknown -> generalGetString(R.string.database_error) - null -> "" // should never be here - } +// val status = chatDbStatus.value +// val title = when (status) { +// is DBMigrationResult.OK -> "" +// is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty()) +// generalGetString(R.string.wrong_passphrase) +// else +// generalGetString(R.string.encrypted_database) +// is DBMigrationResult.ErrorMigration -> when (status.migrationError) { +// is MigrationError.Upgrade -> generalGetString(R.string.database_upgrade) +// is MigrationError.Downgrade -> generalGetString(R.string.database_downgrade) +// is MigrationError.Error -> generalGetString(R.string.incompatible_database_version) +// } +// is DBMigrationResult.ErrorSQL -> generalGetString(R.string.database_error) +// is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error) + // this can only happen if incorrect parameter is passed +// is DBMigrationResult.InvalidConfirmation -> "Invalid migration confirmation" +// is DBMigrationResult.Unknown -> generalGetString(R.string.database_error) +// null -> "" // should never be here +// } Column( Modifier.fillMaxSize().verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center, ) { - Text( - title, - Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), - style = MaterialTheme.typography.h1 - ) - SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { - val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value - when (val status = chatDbStatus.value) { - is DBMigrationResult.ErrorNotADatabase -> { - if (useKeychain && !storedDBKey.isNullOrEmpty()) { +// Text( +// title, +// Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), +// style = MaterialTheme.typography.h1 +// ) +// SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { + val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value + when (val status = chatDbStatus.value) { + is DBMigrationResult.ErrorNotADatabase -> + if (useKeychain && !storedDBKey.isNullOrEmpty()) { + DatabaseErrorDetails(R.string.wrong_passphrase) { Text(generalGetString(R.string.passphrase_is_different)) DatabaseKeyField(dbKey, buttonEnabled) { saveAndRunChatOnClick() @@ -81,7 +91,9 @@ fun DatabaseErrorView( SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) SectionSpacer() Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) - } else { + } + } else { + DatabaseErrorDetails(R.string.encrypted_database) { Text(generalGetString(R.string.database_passphrase_is_required)) DatabaseKeyField(dbKey, buttonEnabled) { if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) @@ -93,36 +105,55 @@ fun DatabaseErrorView( } } } - is DBMigrationResult.Error -> { + is DBMigrationResult.ErrorMigration -> when (status.migrationError) { + is MigrationError.Upgrade -> + DatabaseErrorDetails(R.string.database_upgrade) { + + } + is MigrationError.Downgrade -> + DatabaseErrorDetails(R.string.database_downgrade) { + + } + is MigrationError.Error -> + DatabaseErrorDetails(R.string.incompatible_database_version) { + + } + } + is DBMigrationResult.ErrorSQL -> + DatabaseErrorDetails(R.string.database_error) { Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) - Text(String.format(generalGetString(R.string.error_with_info), status.migrationError)) + Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError)) } - is DBMigrationResult.ErrorKeychain -> { + is DBMigrationResult.ErrorKeychain -> + DatabaseErrorDetails(R.string.keychain_error) { Text(generalGetString(R.string.cannot_access_keychain)) } - is DBMigrationResult.Unknown -> { + is DBMigrationResult.InvalidConfirmation -> + DatabaseErrorDetails(R.string.invalid_migration_confirmation) { + // this can only happen if incorrect parameter is passed + } + is DBMigrationResult.Unknown -> + DatabaseErrorDetails(R.string.database_error) { Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json)) } - is DBMigrationResult.OK -> { - } - null -> { - } - } - if (restoreDbFromBackup.value) { - SectionSpacer() - Text(generalGetString(R.string.database_backup_can_be_restored)) - Spacer(Modifier.size(16.dp)) - RestoreDbButton { - AlertManager.shared.showAlertDialog( - title = generalGetString(R.string.restore_database_alert_title), - text = generalGetString(R.string.restore_database_alert_desc), - confirmText = generalGetString(R.string.restore_database_alert_confirm), - onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) }, - destructive = true, - ) - } + is DBMigrationResult.OK -> {} + null -> {} + } + if (restoreDbFromBackup.value) { + SectionSpacer() + Text(generalGetString(R.string.database_backup_can_be_restored)) + Spacer(Modifier.size(16.dp)) + RestoreDbButton { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.restore_database_alert_title), + text = generalGetString(R.string.restore_database_alert_desc), + confirmText = generalGetString(R.string.restore_database_alert_confirm), + onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) }, + destructive = true, + ) } } +// } } if (progressIndicator.value) { Box( @@ -140,6 +171,16 @@ fun DatabaseErrorView( } } +@Composable +private fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) { + Text( + generalGetString(title), + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) +} + private fun runChat( dbKey: String, chatDbStatus: State, @@ -166,8 +207,8 @@ private fun runChat( is DBMigrationResult.ErrorNotADatabase -> { AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase)) } - is DBMigrationResult.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError) + is DBMigrationResult.ErrorSQL -> { + AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError) } is DBMigrationResult.ErrorKeychain -> { AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt index 0409789e1f..b723c69465 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt @@ -66,8 +66,39 @@ object DatabaseUtils { @Serializable sealed class DBMigrationResult { @Serializable @SerialName("ok") object OK: DBMigrationResult() + @Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult() @Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult() - @Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult() + @Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult() + @Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult() @Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult() @Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult() +} + + +enum class MigrationConfirmation(val value: String) { + YesUp("yesUp"), + YesUpDown ("yesUpDown"), + Error("error") +} + +fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation = + if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + +@Serializable +sealed class MigrationError { + @Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List): MigrationError() + @Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List): MigrationError() + @Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError() +} + +@Serializable +data class UpMigration( + val upName: String, + // val withDown: Boolean +) + +@Serializable +sealed class MTRError { + @Serializable @SerialName("noDown") class NoDown(val dbMigrations: List): MTRError() + @Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError() } \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt new file mode 100644 index 0000000000..204d86dc29 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt @@ -0,0 +1,47 @@ +package chat.simplex.app.views.usersettings + +import SectionDivider +import SectionView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.helpers.AppBarTitle + +@Composable +fun DeveloperView( + m: ChatModel, + showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), + withAuth: (block: () -> Unit) -> Unit +) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + val developerTools = m.controller.appPrefs.developerTools + val confirmDBUpgrades = m.controller.appPrefs.confirmDBUpgrades + val uriHandler = LocalUriHandler.current + AppBarTitle(stringResource(R.string.settings_developer_tools)) + SectionView() { + ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) } + SectionDivider() + val devTools = remember { mutableStateOf(developerTools.get()) } + SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools) + SectionDivider() + var confirm = remember { mutableStateOf(confirmDBUpgrades.get()) } + SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), confirmDBUpgrades, confirm) + SectionDivider() + InstallTerminalAppItem(uriHandler) + SectionDivider() + } + } +} + diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 27c6add259..84ece0f9da 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -57,7 +57,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { chatModel.chatDbEncrypted.value == true, chatModel.incognito, chatModel.controller.appPrefs.incognito, - developerTools = chatModel.controller.appPrefs.developerTools, user.displayName, setPerformLA = setPerformLA, showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, @@ -126,7 +125,6 @@ fun SettingsLayout( encrypted: Boolean, incognito: MutableState, incognitoPref: SharedPreference, - developerTools: SharedPreference, userDisplayName: String, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -207,17 +205,7 @@ fun SettingsLayout( SectionSpacer() SectionView(stringResource(R.string.settings_section_title_develop)) { - val devTools = remember { mutableStateOf(developerTools.get()) } - SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools) - SectionDivider() - if (devTools.value) { - ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) } - SectionDivider() - InstallTerminalAppItem(uriHandler) - SectionDivider() - } -// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) }) -// SectionDivider() + SettingsActionItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) AppVersionItem(showVersion) } } @@ -370,7 +358,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } } -@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) { +@Composable fun ChatConsoleItem(showTerminal: () -> Unit) { SectionItemView(showTerminal) { Icon( painter = painterResource(id = R.drawable.ic_outline_terminal), @@ -382,7 +370,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } } -@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) { +@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(id = R.drawable.ic_github), @@ -544,7 +532,6 @@ fun PreviewSettingsLayout() { encrypted = false, incognito = remember { mutableStateOf(false) }, incognitoPref = SharedPreference({ false }, {}), - developerTools = SharedPreference({ false }, {}), userDisplayName = "Alice", setPerformLA = {}, showModal = { {} }, diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 3cb0dd9a82..388c456727 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -823,6 +823,11 @@ Restore Restore database error Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers. + Database upgrade + Database downgrade + Incompatible database version + Confirm database upgrades + "Invalid migration confirmation" Chat is stopped From 8d8f7b2524906ca5ad6fe4015f100135af6ad227 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Mar 2023 11:26:45 +0100 Subject: [PATCH 2/4] migrations UI --- .../app/views/database/DatabaseErrorView.kt | 126 ++++++++++-------- .../app/src/main/res/values/strings.xml | 6 + 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 623e6049f5..28859f3587 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -14,11 +14,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.AppPreferences +import chat.simplex.app.model.DBEncryptionConfig import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.NotificationsMode @@ -40,45 +42,45 @@ fun DatabaseErrorView( var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } val context = LocalContext.current val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) } - val saveAndRunChatOnClick: () -> Unit = { + + fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { + runChat(dbKey.value, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) + } + + fun saveAndRunChatOnClick() { DatabaseUtils.setDatabaseKey(dbKey.value) storedDBKey = dbKey.value appPreferences.storeDBPassphrase.set(true) useKeychain = true appPreferences.initialRandomDBPassphrase.set(false) - runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) + callRunChat() + } + + @Composable + fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) { + Text( + generalGetString(title), + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) + } + + @Composable + fun fileNameText(dbFile: String) { + Text(String.format(generalGetString(R.string.file_with_path), dbFile)) + } + + @Composable + fun migrationsText(ms: List) { + Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", "))) } -// val status = chatDbStatus.value -// val title = when (status) { -// is DBMigrationResult.OK -> "" -// is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty()) -// generalGetString(R.string.wrong_passphrase) -// else -// generalGetString(R.string.encrypted_database) -// is DBMigrationResult.ErrorMigration -> when (status.migrationError) { -// is MigrationError.Upgrade -> generalGetString(R.string.database_upgrade) -// is MigrationError.Downgrade -> generalGetString(R.string.database_downgrade) -// is MigrationError.Error -> generalGetString(R.string.incompatible_database_version) -// } -// is DBMigrationResult.ErrorSQL -> generalGetString(R.string.database_error) -// is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error) - // this can only happen if incorrect parameter is passed -// is DBMigrationResult.InvalidConfirmation -> "Invalid migration confirmation" -// is DBMigrationResult.Unknown -> generalGetString(R.string.database_error) -// null -> "" // should never be here -// } Column( Modifier.fillMaxSize().verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center, ) { -// Text( -// title, -// Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), -// style = MaterialTheme.typography.h1 -// ) -// SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value when (val status = chatDbStatus.value) { is DBMigrationResult.ErrorNotADatabase -> @@ -88,40 +90,49 @@ fun DatabaseErrorView( DatabaseKeyField(dbKey, buttonEnabled) { saveAndRunChatOnClick() } - SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) SectionSpacer() - Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) + fileNameText(status.dbFile) } } else { DatabaseErrorDetails(R.string.encrypted_database) { Text(generalGetString(R.string.database_passphrase_is_required)) - DatabaseKeyField(dbKey, buttonEnabled) { - if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) - } if (useKeychain) { - SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick) + SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) } else { - OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } + DatabaseKeyField(dbKey, buttonEnabled, { callRunChat() }) + OpenChatButton(buttonEnabled) { callRunChat() } } } } - is DBMigrationResult.ErrorMigration -> when (status.migrationError) { + is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { is MigrationError.Upgrade -> DatabaseErrorDetails(R.string.database_upgrade) { - + TextButton({ callRunChat(MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + Text(generalGetString(R.string.upgrade_and_open_chat)) + } + fileNameText(status.dbFile) + migrationsText(err.upMigrations.map { it.upName }) } is MigrationError.Downgrade -> DatabaseErrorDetails(R.string.database_downgrade) { - + Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold) + TextButton({ callRunChat(MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + Text(generalGetString(R.string.downgrade_and_open_chat)) + } + fileNameText(status.dbFile) + migrationsText(err.downMigrations) } is MigrationError.Error -> DatabaseErrorDetails(R.string.incompatible_database_version) { - + fileNameText(status.dbFile) + Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError))) } } is DBMigrationResult.ErrorSQL -> DatabaseErrorDetails(R.string.database_error) { - Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) + fileNameText(status.dbFile) Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError)) } is DBMigrationResult.ErrorKeychain -> @@ -153,7 +164,6 @@ fun DatabaseErrorView( ) } } -// } } if (progressIndicator.value) { Box( @@ -171,18 +181,9 @@ fun DatabaseErrorView( } } -@Composable -private fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) { - Text( - generalGetString(title), - Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), - style = MaterialTheme.typography.h1 - ) - SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) -} - private fun runChat( dbKey: String, + confirmMigrations: MigrationConfirmation? = null, chatDbStatus: State, progressIndicator: MutableState, prefs: AppPreferences @@ -191,7 +192,7 @@ private fun runChat( if (progressIndicator.value) return@launch progressIndicator.value = true try { - SimplexApp.context.initChatController(dbKey) + SimplexApp.context.initChatController(dbKey, confirmMigrations) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } @@ -204,18 +205,17 @@ private fun runChat( NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() } } - is DBMigrationResult.ErrorNotADatabase -> { + is DBMigrationResult.ErrorNotADatabase -> AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase)) - } - is DBMigrationResult.ErrorSQL -> { + is DBMigrationResult.ErrorSQL -> AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError) - } - is DBMigrationResult.ErrorKeychain -> { + is DBMigrationResult.ErrorKeychain -> AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error)) - } - is DBMigrationResult.Unknown -> { + is DBMigrationResult.Unknown -> AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json) - } + is DBMigrationResult.InvalidConfirmation -> + AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation)) + is DBMigrationResult.ErrorMigration -> {} null -> {} } } @@ -245,6 +245,14 @@ private fun restoreDb(restoreDbFromBackup: MutableState, prefs: AppPref } } +private fun mtrErrorDescription(err: MTRError): String = + when (err) { + is MTRError.NoDown -> + String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", ")) + is MTRError.Different -> + String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration) + } + @Composable private fun DatabaseKeyField(text: MutableState, enabled: Boolean, onClick: (() -> Unit)? = null) { PassphraseField( diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 50566c333e..e7f7ec87e7 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -829,6 +829,12 @@ Incompatible database version Confirm database upgrades "Invalid migration confirmation" + Upgrade and open chat + Downgrade and open chat + database version is newer than the app, but no down migration for: %s + different migration in the app/database: %s / %s + Migrations: %s + "Warning: you may lose some data!" Chat is stopped From c3dffc5909845cb7c499f878a9e20ddd881b173f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:14:09 +0100 Subject: [PATCH 3/4] fix --- .../app/views/database/DatabaseErrorView.kt | 22 ++++++++++++------- .../app/views/usersettings/SettingsView.kt | 8 ++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 28859f3587..84e6714903 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -23,6 +23,7 @@ import chat.simplex.app.model.AppPreferences import chat.simplex.app.model.DBEncryptionConfig import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.AppVersionText import chat.simplex.app.views.usersettings.NotificationsMode import kotlinx.coroutines.* import kotlinx.datetime.Clock @@ -44,7 +45,8 @@ fun DatabaseErrorView( val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) } fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { - runChat(dbKey.value, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) + val useKey = if (useKeychain) null else dbKey.value + runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) } fun saveAndRunChatOnClick() { @@ -60,7 +62,7 @@ fun DatabaseErrorView( fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) { Text( generalGetString(title), - Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp), style = MaterialTheme.typography.h1 ) SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) @@ -68,7 +70,7 @@ fun DatabaseErrorView( @Composable fun fileNameText(dbFile: String) { - Text(String.format(generalGetString(R.string.file_with_path), dbFile)) + Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile)) } @Composable @@ -101,7 +103,7 @@ fun DatabaseErrorView( DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick) SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) } else { - DatabaseKeyField(dbKey, buttonEnabled, { callRunChat() }) + DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() } OpenChatButton(buttonEnabled) { callRunChat() } } } @@ -109,20 +111,24 @@ fun DatabaseErrorView( is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { is MigrationError.Upgrade -> DatabaseErrorDetails(R.string.database_upgrade) { - TextButton({ callRunChat(MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(R.string.upgrade_and_open_chat)) } + Spacer(Modifier.height(20.dp)) fileNameText(status.dbFile) migrationsText(err.upMigrations.map { it.upName }) + AppVersionText() } is MigrationError.Downgrade -> DatabaseErrorDetails(R.string.database_downgrade) { - Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold) - TextButton({ callRunChat(MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { + TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(R.string.downgrade_and_open_chat)) } + Spacer(Modifier.height(20.dp)) + Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold) fileNameText(status.dbFile) migrationsText(err.downMigrations) + AppVersionText() } is MigrationError.Error -> DatabaseErrorDetails(R.string.incompatible_database_version) { @@ -182,7 +188,7 @@ fun DatabaseErrorView( } private fun runChat( - dbKey: String, + dbKey: String? = null, confirmMigrations: MigrationConfirmation? = null, chatDbStatus: State, progressIndicator: MutableState, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index d75ca382bb..f55eb941ba 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -386,9 +386,11 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } @Composable private fun AppVersionItem(showVersion: () -> Unit) { - SectionItemView(showVersion) { - Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") - } + SectionItemView(showVersion) { AppVersionText() } +} + +@Composable fun AppVersionText() { + Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } @Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) { From 4895f396a223826b8cec91b839652a636fd60a32 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:00:18 +0100 Subject: [PATCH 4/4] corrections --- .../app/views/database/DatabaseErrorView.kt | 19 +++++++++---------- .../app/views/usersettings/DeveloperView.kt | 6 +----- .../app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 84e6714903..05a8288ada 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.AppPreferences -import chat.simplex.app.model.DBEncryptionConfig import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.AppVersionText @@ -69,12 +68,12 @@ fun DatabaseErrorView( } @Composable - fun fileNameText(dbFile: String) { + fun FileNameText(dbFile: String) { Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile)) } @Composable - fun migrationsText(ms: List) { + fun MigrationsText(ms: List) { Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", "))) } @@ -94,7 +93,7 @@ fun DatabaseErrorView( } SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick) SectionSpacer() - fileNameText(status.dbFile) + FileNameText(status.dbFile) } } else { DatabaseErrorDetails(R.string.encrypted_database) { @@ -115,8 +114,8 @@ fun DatabaseErrorView( Text(generalGetString(R.string.upgrade_and_open_chat)) } Spacer(Modifier.height(20.dp)) - fileNameText(status.dbFile) - migrationsText(err.upMigrations.map { it.upName }) + FileNameText(status.dbFile) + MigrationsText(err.upMigrations.map { it.upName }) AppVersionText() } is MigrationError.Downgrade -> @@ -126,19 +125,19 @@ fun DatabaseErrorView( } Spacer(Modifier.height(20.dp)) Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold) - fileNameText(status.dbFile) - migrationsText(err.downMigrations) + FileNameText(status.dbFile) + MigrationsText(err.downMigrations) AppVersionText() } is MigrationError.Error -> DatabaseErrorDetails(R.string.incompatible_database_version) { - fileNameText(status.dbFile) + FileNameText(status.dbFile) Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError))) } } is DBMigrationResult.ErrorSQL -> DatabaseErrorDetails(R.string.database_error) { - fileNameText(status.dbFile) + FileNameText(status.dbFile) Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError)) } is DBMigrationResult.ErrorKeychain -> diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt index 204d86dc29..e0fd6d276f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt @@ -22,10 +22,7 @@ fun DeveloperView( showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), withAuth: (block: () -> Unit) -> Unit ) { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start - ) { + Column(Modifier.fillMaxWidth()) { val developerTools = m.controller.appPrefs.developerTools val confirmDBUpgrades = m.controller.appPrefs.confirmDBUpgrades val uriHandler = LocalUriHandler.current @@ -40,7 +37,6 @@ fun DeveloperView( SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), confirmDBUpgrades, confirm) SectionDivider() InstallTerminalAppItem(uriHandler) - SectionDivider() } } } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index e7f7ec87e7..2f8206c58a 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -828,13 +828,13 @@ Database downgrade Incompatible database version Confirm database upgrades - "Invalid migration confirmation" + Invalid migration confirmation Upgrade and open chat Downgrade and open chat database version is newer than the app, but no down migration for: %s different migration in the app/database: %s / %s Migrations: %s - "Warning: you may lose some data!" + Warning: you may lose some data! Chat is stopped