diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxErrorDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxErrorDialog.kt index 6f64ae07ccc88a15c4cf35ba3654e5a3a09ea83c..6cdf895cefb2adb7449d91d7335f07be502c8b20 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxErrorDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxErrorDialog.kt @@ -34,6 +34,8 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberDialogState import org.briarproject.bramble.api.mailbox.MailboxPairingState import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.OfflineWhenPairing +import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.Pairing +import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.WasUnpaired import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview import java.awt.Dimension @@ -86,7 +88,7 @@ fun MailboxErrorDialog( private fun MailboxPairingUiState.getError(): String = when (this) { is OfflineWhenPairing -> i18n("mailbox.setup.offline_error_title") + "\n\n" + i18n("mailbox.setup.offline_error_description") - is MailboxPairingUiState.Pairing -> when (val s = pairingState) { + is Pairing -> when (val s = pairingState) { is MailboxPairingState.InvalidQrCode -> i18n("mailbox.setup.link.error") is MailboxPairingState.MailboxAlreadyPaired -> i18n("mailbox.setup.already_paired_title") + "\n\n" + i18n("mailbox.setup.already_paired_description") @@ -95,5 +97,7 @@ private fun MailboxPairingUiState.getError(): String = when (this) { is MailboxPairingState.UnexpectedError -> i18n("mailbox.setup.assertion_error_description") else -> error("Unhandled pairing state: ${s::class.simpleName}") } + is WasUnpaired -> if (tellUserToWipeMailbox) i18n("mailbox.unlink.no_wipe.title") + + "\n\n" + i18n("mailbox.unlink.no_wipe.message") else i18n("mailbox.unlink.no_wipe.title") else -> error("Unhandled state: ${this::class.simpleName}") } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxScreen.kt index 27b2a02a586f9ee9d4dec822a8e62c30b6dae949..c9920763af3bf57a5f1f2c5f089a7debb0da6ab8 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxScreen.kt @@ -32,7 +32,6 @@ import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.Pairing import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.Unknown import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.WasUnpaired import org.briarproject.briar.desktop.ui.Loader -import org.briarproject.briar.desktop.ui.UiPlaceholder import org.briarproject.briar.desktop.viewmodel.viewModel @Composable @@ -49,6 +48,8 @@ fun MailboxScreen(viewModel: MailboxViewModel = viewModel()) { status = viewModel.status.value, isCheckingConnection = false, // we just paired, there was no time to trigger a connection check onCheckConnection = viewModel::checkConnection, + onWipe = viewModel::unlink, + isWiping = false, // same as above, just paired ) } OfflineWhenPairing -> MailboxSetupScreen(viewModel, true) @@ -56,7 +57,10 @@ fun MailboxScreen(viewModel: MailboxViewModel = viewModel()) { status = viewModel.status.value, isCheckingConnection = state.connectionCheckRunning, onCheckConnection = viewModel::checkConnection, + onWipe = viewModel::unlink, + isWiping = state.isWiping, ) - is WasUnpaired -> UiPlaceholder() // TODO + // the unpair success message will be shown like an error dialog + is WasUnpaired -> MailboxSetupScreen(viewModel, true) } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxStatusScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxStatusScreen.kt index 7464fa46015f6300d3964207dee5f7b874f9c47e..e23f2ab7d5dbcb2b9fc0bd65a20ea8134fea1d08 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxStatusScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxStatusScreen.kt @@ -20,12 +20,19 @@ package org.briarproject.briar.desktop.mailbox import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize.Max import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.ButtonType.DESTRUCTIVE +import androidx.compose.material.ButtonType.NEUTRAL import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.DialogButton +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton @@ -35,6 +42,8 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.QuestionMark import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -62,6 +71,7 @@ fun main() = preview( listOf("error", "problem", "mailbox too old", "briar too old", "ok") ), "isCheckingConnection" to false, + "isWiping" to false, ) { val now = System.currentTimeMillis() val status = when (getStringParameter("status")) { @@ -71,7 +81,13 @@ fun main() = preview( "briar too old" -> MailboxStatus(now, now, 0, listOf(MailboxVersion(42, 0))) else -> MailboxStatus(now, now - 18_354, 0, CLIENT_SUPPORTS) } - MailboxStatusScreen(status, getBooleanParameter("isCheckingConnection")) {} + MailboxStatusScreen( + status = status, + isCheckingConnection = getBooleanParameter("isCheckingConnection"), + onCheckConnection = {}, + isWiping = getBooleanParameter("isWiping"), + onWipe = {}, + ) } @Composable @@ -79,8 +95,11 @@ fun MailboxStatusScreen( status: MailboxStatus?, isCheckingConnection: Boolean, onCheckConnection: () -> Unit, + isWiping: Boolean, + onWipe: () -> Unit, ) { if (status == null) return // not expected to happen (for a noticeable amount of time) + val deleteGroupDialogVisible = remember { mutableStateOf(false) } Column( verticalArrangement = spacedBy(16.dp), horizontalAlignment = CenterHorizontally, @@ -129,20 +148,27 @@ fun MailboxStatusScreen( } if (isCheckingConnection) CircularProgressIndicator() else Button( - onClick = onCheckConnection + onClick = onCheckConnection, + enabled = !isWiping, ) { Text( text = i18n("mailbox.status.check.connection.button"), ) } Spacer(modifier = Modifier.weight(1f)) - OutlinedButton( - onClick = { /* TODO */ }, + if (isWiping) CircularProgressIndicator() + else OutlinedButton( + onClick = { deleteGroupDialogVisible.value = true }, + enabled = !isCheckingConnection, ) { Text( text = i18n("mailbox.status.unlink.button"), ) } + if (deleteGroupDialogVisible.value) MailboxWipeDialog( + close = { deleteGroupDialogVisible.value = false }, + onWipe = onWipe, + ) } } @@ -174,3 +200,44 @@ private fun MailboxStatusView( modifier = Modifier.alpha(0.56f) ) } + +@Composable +@OptIn(ExperimentalMaterialApi::class) +fun MailboxWipeDialog( + close: () -> Unit, + onWipe: () -> Unit = {}, +) { + AlertDialog( + onDismissRequest = close, + title = { + Text( + text = i18n("mailbox.unlink.dialog.title"), + modifier = Modifier.width(Max), + style = MaterialTheme.typography.h6, + ) + }, + text = { + Column(verticalArrangement = spacedBy(16.dp)) { + Text(i18n("mailbox.unlink.dialog.question")) + Text(i18n("mailbox.unlink.dialog.warning")) + } + }, + dismissButton = { + DialogButton( + onClick = { close() }, + text = i18n("cancel"), + type = NEUTRAL, + ) + }, + confirmButton = { + DialogButton( + onClick = { + close() + onWipe() + }, + text = i18n("mailbox.unlink.dialog.button"), + type = DESTRUCTIVE, + ) + }, + ) +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxViewModel.kt index 8e293a210cf0f570ece116259ebf013b305d2ec3..50fd5e3ada166edd63ae8323f48545e68005b1ab 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/mailbox/MailboxViewModel.kt @@ -40,6 +40,7 @@ import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.NotSetup import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.OfflineWhenPairing import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.Pairing import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.Unknown +import org.briarproject.briar.desktop.mailbox.MailboxPairingUiState.WasUnpaired import org.briarproject.briar.desktop.threading.BriarExecutors import org.briarproject.briar.desktop.threading.UiExecutor import org.briarproject.briar.desktop.utils.KLoggerUtils.i @@ -53,7 +54,7 @@ sealed class MailboxPairingUiState { object NotSetup : MailboxPairingUiState() class Pairing(val pairingState: MailboxPairingState) : MailboxPairingUiState() object OfflineWhenPairing : MailboxPairingUiState() - class IsPaired(val connectionCheckRunning: Boolean) : MailboxPairingUiState() + class IsPaired(val connectionCheckRunning: Boolean, val isWiping: Boolean) : MailboxPairingUiState() class WasUnpaired(val tellUserToWipeMailbox: Boolean) : MailboxPairingUiState() } @@ -93,7 +94,7 @@ class MailboxViewModel @Inject constructor( if (isPaired) { val mailboxStatus = mailboxManager.getMailboxStatus(txn) briarExecutors.onUiThread { - _pairingState.value = IsPaired(false) + _pairingState.value = IsPaired(connectionCheckRunning = false, isWiping = false) _status.value = mailboxStatus } } else briarExecutors.onUiThread { @@ -160,16 +161,27 @@ class MailboxViewModel @Inject constructor( @UiExecutor fun checkConnection() { // we can only check the connection when we are already paired (or just finished pairing) - _pairingState.value = IsPaired(true) + _pairingState.value = IsPaired(connectionCheckRunning = true, isWiping = false) briarExecutors.onIoThread { // this check updates _status state via an Event val success = mailboxManager.checkConnection() LOG.i { "Got result from connection check: $success" } briarExecutors.onUiThread { val s = pairingState.value - if (s is IsPaired) _pairingState.value = IsPaired(false) + if (s is IsPaired) _pairingState.value = IsPaired(connectionCheckRunning = false, isWiping = false) else LOG.w { "Unexpected state: ${s::class.simpleName}" } } } } + + @UiExecutor + fun unlink() { + _pairingState.value = IsPaired(connectionCheckRunning = false, isWiping = true) + briarExecutors.onIoThread { + val wasWiped = mailboxManager.unPair() + briarExecutors.onUiThread { + _pairingState.value = WasUnpaired(!wasWiped) + } + } + } } diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index 58fd1974211169486caa894c92a8b0c79fec231a..a317839da5e585027ed9823345500a69e47444b9 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -261,6 +261,12 @@ mailbox.setup.io_error_description=Ensure that both devices are connected to the mailbox.setup.assertion_error_description=Please create a bug report if the issue persists. mailbox.setup.offline_error_title=Offline mailbox.setup.offline_error_description=Ensure that you are online and try again after a while. +mailbox.unlink.no_wipe.title=Your Mailbox has been unlinked +mailbox.unlink.no_wipe.message=Next time you have access to your Mailbox device, please open the Mailbox app and tap the "Unlink" button to complete the process.\n\nIf you no longer have access to your Mailbox device, don't worry. Your data is encrypted so it will remain secure even if you don't complete the process. +mailbox.unlink.dialog.title=Unlink mailbox? +mailbox.unlink.dialog.question=Are you sure you want to unlink your Mailbox? +mailbox.unlink.dialog.warning=If you unlink your Mailbox, you won't be able to receive messages while Briar is offline. +mailbox.unlink.dialog.button=Unlink mailbox.status.error=Mailbox is unavailable mailbox.status.problem=Briar is having trouble connecting to the Mailbox mailbox.status.app_too_old.title=Briar is too old