diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt index e972bf95884c6fa8cd4e8168cc9ff2fa657d28df..ada8c83c24bf45bd2d627af608026913572e889d 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester @@ -43,9 +44,13 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.briarproject.bramble.api.crypto.DecryptionResult import org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD import org.briarproject.briar.desktop.login.PasswordForm +import org.briarproject.briar.desktop.settings.ChangePasswordSubViewModel.DialogState +import org.briarproject.briar.desktop.ui.ModalLoader import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview @@ -64,17 +69,21 @@ fun main() = preview( val passwordTooWeakError = derivedStateOf { passwordStrength.value < 0.6f } - val (submitError, setSubmitError) = remember { mutableStateOf<DecryptionResult?>(null) } + val (dialogState, setDialogState) = remember { mutableStateOf<DialogState>(DialogState.Idle) } + val scope = rememberCoroutineScope() ChangePasswordDialog( isVisible = getBooleanParameter("visible"), close = { setBooleanParameter("visible", false) }, - onChange = { - // use password with 8 or less to test failure - if (password.length <= 8) { - setSubmitError(INVALID_PASSWORD) - false - } else { - true + confirmChange = { + setDialogState(DialogState.Loading) + scope.launch { + // simulate loading for 1s + delay(1000) + // use old password with 8 or fewer characters to test failure + if (oldPassword.length <= 8) + setDialogState(DialogState.Error(INVALID_PASSWORD)) + else + setDialogState(DialogState.Done) } }, oldPassword = oldPassword, @@ -87,7 +96,13 @@ fun main() = preview( passwordTooWeakError = passwordTooWeakError.value, passwordsDontMatchError = password != passwordConfirmation, buttonEnabled = !passwordTooWeakError.value && password == passwordConfirmation, - submitError = submitError, + reset = { + setOldPassword("") + setPassword("") + setPasswordConfirmation("") + setDialogState(DialogState.Idle) + }, + dialogState = dialogState, ) } @@ -96,31 +111,30 @@ fun ChangePasswordDialog( isVisible: Boolean, close: () -> Unit, viewHolder: ChangePasswordSubViewModel, -) { - ChangePasswordDialog( - isVisible = isVisible, - close = close, - onChange = viewHolder::confirmChange, - oldPassword = viewHolder.oldPassword.value, - setOldPassword = viewHolder::setOldPassword, - password = viewHolder.password.value, - setPassword = viewHolder::setPassword, - passwordConfirmation = viewHolder.passwordConfirmation.value, - setPasswordConfirmation = viewHolder::setPasswordConfirmation, - passwordStrength = viewHolder.passwordStrength.value, - passwordTooWeakError = viewHolder.passwordTooWeakError.value, - passwordsDontMatchError = viewHolder.passwordMatchError.value, - buttonEnabled = viewHolder.buttonEnabled.value, - submitError = viewHolder.submitError.value, - ) -} +) = ChangePasswordDialog( + isVisible = isVisible, + close = close, + confirmChange = viewHolder::confirmChange, + oldPassword = viewHolder.oldPassword.value, + setOldPassword = viewHolder::setOldPassword, + password = viewHolder.password.value, + setPassword = viewHolder::setPassword, + passwordConfirmation = viewHolder.passwordConfirmation.value, + setPasswordConfirmation = viewHolder::setPasswordConfirmation, + passwordStrength = viewHolder.passwordStrength.value, + passwordTooWeakError = viewHolder.passwordTooWeakError.value, + passwordsDontMatchError = viewHolder.passwordMatchError.value, + buttonEnabled = viewHolder.buttonEnabled.value, + reset = viewHolder::reset, + dialogState = viewHolder.dialogState.value, +) @OptIn(ExperimentalMaterialApi::class) @Composable fun ChangePasswordDialog( isVisible: Boolean, close: () -> Unit, - onChange: () -> Boolean, + confirmChange: () -> Unit, oldPassword: String, setOldPassword: (String) -> Unit, password: String, @@ -131,21 +145,20 @@ fun ChangePasswordDialog( passwordTooWeakError: Boolean, passwordsDontMatchError: Boolean, buttonEnabled: Boolean, - submitError: DecryptionResult?, + reset: () -> Unit, + dialogState: DialogState, ) { if (!isVisible) return - val onClose = { - setOldPassword("") - setPassword("") - setPasswordConfirmation("") - close() + val onClose = remember { + { + reset() + close() + } } - val onSubmit = { - if (onChange()) { - onClose() - } + LaunchedEffect(dialogState) { + if (dialogState is DialogState.Done) onClose() } AlertDialog( @@ -168,8 +181,8 @@ fun ChangePasswordDialog( passwordStrength = passwordStrength, passwordTooWeakError = passwordTooWeakError, passwordsDontMatchError = passwordsDontMatchError, - onSubmit = onSubmit, - submitError = submitError, + onSubmit = confirmChange, + submitError = (dialogState as? DialogState.Error)?.result, ) }, dismissButton = { @@ -181,13 +194,17 @@ fun ChangePasswordDialog( }, confirmButton = { DialogButton( - onClick = onSubmit, + onClick = confirmChange, text = i18n("change"), type = ButtonType.NEUTRAL, enabled = buttonEnabled, ) }, ) + + if (dialogState is DialogState.Loading) { + ModalLoader(i18n("settings.security.password.changing")) + } } @Composable diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordSubViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordSubViewModel.kt index c836584c6c66e6dd73dfb0d03688127a06a1558c..39d03a085fd78de8970634fd9d59172fd87b5d72 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordSubViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordSubViewModel.kt @@ -25,17 +25,29 @@ import org.briarproject.bramble.api.crypto.DecryptionException import org.briarproject.bramble.api.crypto.DecryptionResult import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK +import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.viewmodel.asState +import javax.inject.Inject -class ChangePasswordSubViewModel( +class ChangePasswordSubViewModel +@Inject +constructor( + private val briarExecutors: BriarExecutors, private val accountManager: AccountManager, private val passwordStrengthEstimator: PasswordStrengthEstimator, ) { + sealed class DialogState { + object Idle : DialogState() + object Loading : DialogState() + object Done : DialogState() + class Error(val result: DecryptionResult) : DialogState() + } + private val _oldPassword = mutableStateOf("") private val _password = mutableStateOf("") private val _passwordConfirmation = mutableStateOf("") - private val _submitError = mutableStateOf<DecryptionResult?>(null) + private val _dialogState = mutableStateOf<DialogState>(DialogState.Idle) val oldPassword = _oldPassword.asState() val password = _password.asState() @@ -57,7 +69,7 @@ class ChangePasswordSubViewModel( !passwordTooWeakError.value && !passwordMatchError.value } - val submitError = _submitError.asState() + val dialogState = _dialogState.asState() fun setOldPassword(password: String) { _oldPassword.value = password @@ -71,15 +83,24 @@ class ChangePasswordSubViewModel( _passwordConfirmation.value = passwordConfirmation } - fun confirmChange(): Boolean { - if (!buttonEnabled.value) return false - return try { - accountManager.changePassword(oldPassword.value, _password.value) - _submitError.value = null - true - } catch (e: DecryptionException) { - _submitError.value = e.decryptionResult - false + fun confirmChange() { + if (!buttonEnabled.value) return + + _dialogState.value = DialogState.Loading + briarExecutors.onIoThread { + try { + accountManager.changePassword(_oldPassword.value, _password.value) + _dialogState.value = DialogState.Done + } catch (e: DecryptionException) { + _dialogState.value = DialogState.Error(e.decryptionResult) + } } } + + fun reset() { + setOldPassword("") + setPassword("") + setPasswordConfirmation("") + _dialogState.value = DialogState.Idle + } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt index 382fa37eca4e1d4100ce64206561c4d8ce62c404..54882686a48455e4a43eef64eb81c870f971dde1 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt @@ -19,8 +19,6 @@ package org.briarproject.briar.desktop.settings import androidx.compose.runtime.mutableStateOf -import org.briarproject.bramble.api.account.AccountManager -import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator import org.briarproject.bramble.api.db.TransactionManager import org.briarproject.bramble.api.lifecycle.LifecycleManager import org.briarproject.briar.desktop.notification.VisualNotificationProvider @@ -43,13 +41,12 @@ enum class SettingCategory { class SettingsViewModel @Inject constructor( + val changePasswordSubViewModel: ChangePasswordSubViewModel, private val briarExecutors: BriarExecutors, lifecycleManager: LifecycleManager, db: TransactionManager, private val unencryptedSettings: UnencryptedSettings, private val encryptedSettings: EncryptedSettings, - private val accountManager: AccountManager, - private val passwordStrengthEstimator: PasswordStrengthEstimator, private val visualNotificationProvider: VisualNotificationProvider, ) : DbViewModel(briarExecutors, lifecycleManager, db) { private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY) @@ -67,8 +64,6 @@ constructor( private val _changePasswordDialogVisible = mutableStateOf(false) val changePasswordDialogVisible = _changePasswordDialogVisible.asState() - val changePasswordSubViewModel = ChangePasswordSubViewModel(accountManager, passwordStrengthEstimator) - private val _visualNotifications = mutableStateOf(encryptedSettings.visualNotifications) val visualNotifications = _visualNotifications.asState() diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt index 6328571a4322dd9ee30e9d3da52dcc09554caa1b..084b116bba9f40cadbd7771b81042762254fba04 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt @@ -19,22 +19,47 @@ package org.briarproject.briar.desktop.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.AlertDialog import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun Loader() { +fun Loader() = Box( - contentAlignment = Alignment.Center, + contentAlignment = Center, modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background).padding(20.dp) ) { CircularProgressIndicator() } -} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ModalLoader(text: String? = null) = AlertDialog( + modifier = Modifier.padding(top = 16.dp), + onDismissRequest = {}, + buttons = {}, + text = { + Row( + horizontalArrangement = spacedBy(16.dp, alignment = CenterHorizontally), + verticalAlignment = CenterVertically, + modifier = Modifier.widthIn(min = 200.dp) + ) { + CircularProgressIndicator() + text?.let { Text(text) } + } + }, +) diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index 0ce9d2ea98168a3a8ee7cb7198ccab277599d181..350ba0586c61e7c9e92f55e62b6d20d2ac4d32e5 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -323,6 +323,7 @@ settings.security.password.change=Change password settings.security.password.current=Current password settings.security.password.choose=New password settings.security.password.confirm=Confirm new password +settings.security.password.changing=Changing password… settings.security.password.changed=Password has been changed. # Settings Notifications