diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt index b99c731bf2656a7e0bd3ac7d15f640b92a06ed35..edf4a93aff3a7640a7d324bbd11ff953f208a287 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt @@ -22,25 +22,26 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable fun ContactDropDown( expanded: Boolean, - isExpanded: (Boolean) -> Unit, + close: () -> Unit, onMakeIntroduction: () -> Unit, + onDeleteAllMessages: () -> Unit, ) { var connectionMode by remember { mutableStateOf(false) } var contactMode by remember { mutableStateOf(false) } DropdownMenu( expanded = expanded, - onDismissRequest = { isExpanded(false) }, + onDismissRequest = close, ) { - DropdownMenuItem(onClick = { isExpanded(false); onMakeIntroduction() }) { + DropdownMenuItem(onClick = { close(); onMakeIntroduction() }) { Text(i18n("contacts.dropdown.introduction"), fontSize = 14.sp) } DropdownMenuItem(onClick = {}) { Text(i18n("contacts.dropdown.disappearing"), fontSize = 14.sp) } - DropdownMenuItem(onClick = {}) { + DropdownMenuItem(onClick = { close(); onDeleteAllMessages() }) { Text(i18n("contacts.dropdown.delete.all"), fontSize = 14.sp) } - DropdownMenuItem(onClick = { connectionMode = true; isExpanded(false) }) { + DropdownMenuItem(onClick = { connectionMode = true; close() }) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( i18n("contacts.dropdown.connections"), @@ -54,7 +55,7 @@ fun ContactDropDown( ) } } - DropdownMenuItem(onClick = { contactMode = true; isExpanded(false) }) { + DropdownMenuItem(onClick = { contactMode = true; close() }) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( i18n("contacts.dropdown.contact"), @@ -69,36 +70,32 @@ fun ContactDropDown( } } } - if (connectionMode) { - DropdownMenu( - expanded = connectionMode, - onDismissRequest = { connectionMode = false }, - ) { - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.connections.title"), fontSize = 12.sp) - } - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.connections.bluetooth"), fontSize = 14.sp) - } - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.connections.removable"), fontSize = 14.sp) - } + DropdownMenu( + expanded = connectionMode, + onDismissRequest = { connectionMode = false }, + ) { + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.connections.title"), fontSize = 12.sp) + } + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.connections.bluetooth"), fontSize = 14.sp) + } + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.connections.removable"), fontSize = 14.sp) } } - if (contactMode) { - DropdownMenu( - expanded = contactMode, - onDismissRequest = { contactMode = false }, - ) { - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.contact.title"), fontSize = 12.sp) - } - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.contact.change"), fontSize = 14.sp) - } - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.contact.delete"), fontSize = 14.sp) - } + DropdownMenu( + expanded = contactMode, + onDismissRequest = { contactMode = false }, + ) { + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.contact.title"), fontSize = 12.sp) + } + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.contact.change"), fontSize = 14.sp) + } + DropdownMenuItem(onClick = { false }) { + Text(i18n("contacts.dropdown.contact.delete"), fontSize = 14.sp) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt new file mode 100644 index 0000000000000000000000000000000000000000..136f61939ca2bb73e91abdd7dfba5b81ba9a7d34 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt @@ -0,0 +1,140 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.width +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import org.briarproject.briar.api.conversation.DeletionResult +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.PreviewUtils.preview + +fun main() = preview( + "introduction_pending" to false, + "invitation_pending" to false, + "introduction_not_all" to false, + "invitation_not_all" to false, +) { + var confirmationDialog by remember { mutableStateOf(false) } + var failedDialog by remember { mutableStateOf(false) } + val deletionResult by derivedStateOf { + if (!failedDialog) null else + DeletionResult().apply { + if (getBooleanParameter("introduction_pending")) addIntroductionSessionInProgress() + if (getBooleanParameter("invitation_pending")) addInvitationSessionInProgress() + if (getBooleanParameter("introduction_not_all")) addIntroductionNotAllSelected() + if (getBooleanParameter("invitation_not_all")) addInvitationNotAllSelected() + } + } + + Column { + Button(onClick = { confirmationDialog = true }) { + Text("Show confirmation dialog") + } + + Button(onClick = { failedDialog = true }) { + Text("Show deletion failed dialog") + } + } + + DeleteAllMessagesConfirmationDialog( + isVisible = confirmationDialog, + close = { confirmationDialog = false }, + ) + + DeleteAllMessagesFailedDialog(deletionResult) { failedDialog = false } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DeleteAllMessagesConfirmationDialog( + isVisible: Boolean, + close: () -> Unit, + onDelete: () -> Unit = {}, + onCancel: () -> Unit = {}, +) { + if (!isVisible) return + + AlertDialog( + onDismissRequest = close, + title = { + Text( + text = i18n("conversation.delete.all.dialog.title"), + modifier = Modifier.width(IntrinsicSize.Max) + ) + }, + text = { + Text(i18n("conversation.delete.all.dialog.message")) + }, + dismissButton = { + TextButton(onClick = { close(); onDelete() }) { + Text(i18n("delete")) + } + }, + confirmButton = { + TextButton(onClick = { close(); onCancel() }) { + Text(i18n("cancel")) + } + } + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DeleteAllMessagesFailedDialog( + deletionResult: DeletionResult?, + close: () -> Unit, +) { + if (deletionResult == null) return + + val message = buildList { + when { + // get failures the user cannot immediately resolve + deletionResult.hasIntroductionSessionInProgress() && + deletionResult.hasInvitationSessionInProgress() -> + add(i18n("conversation.delete.failed.dialog.message.ongoing_both")) + deletionResult.hasIntroductionSessionInProgress() -> + add(i18n("conversation.delete.failed.dialog.message.ongoing_introductions")) + deletionResult.hasInvitationSessionInProgress() -> + add(i18n("conversation.delete.failed.dialog.message.ongoing_invitations")) + } + when { + // add problems the user can resolve + deletionResult.hasNotAllIntroductionSelected() && + deletionResult.hasNotAllInvitationSelected() -> + add(i18n("conversation.delete.failed.dialog.message.not_all_selected_both")) + deletionResult.hasNotAllIntroductionSelected() -> + add(i18n("conversation.delete.failed.dialog.message.not_all_selected_introductions")) + deletionResult.hasNotAllInvitationSelected() -> + add(i18n("conversation.delete.failed.dialog.message.not_all_selected_invitations")) + } + }.joinToString("\n\n") + + AlertDialog( + onDismissRequest = close, + title = { + Text( + text = i18n("conversation.delete.failed.dialog.title"), + modifier = Modifier.width(IntrinsicSize.Max) + ) + }, + text = { + Text(message) + }, + confirmButton = { + TextButton(onClick = close) { + Text(i18n("ok")) + } + } + ) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt index a6566260d67adf60248d6230b267d161bcf486fc..87edcda219c24910d3b5d41402fe864437640d84 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt @@ -34,6 +34,7 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n fun ConversationHeader( contactItem: ContactItem, onMakeIntroduction: () -> Unit, + onDeleteAllMessages: () -> Unit, ) { val (isExpanded, setExpanded) = remember { mutableStateOf(false) } val onlineColor = @@ -64,7 +65,7 @@ fun ConversationHeader( modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp) ) { Icon(Icons.Filled.MoreVert, i18n("access.contact.menu"), modifier = Modifier.size(24.dp)) - ContactDropDown(isExpanded, setExpanded, onMakeIntroduction) + ContactDropDown(isExpanded, { setExpanded(false) }, onMakeIntroduction, onDeleteAllMessages) } HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt index e10fe586a45effdf5250ccfbc14b2ffd4ea8c16c..29618666a31b89e010c632849e1332d90c9c8fa3 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH import org.briarproject.briar.desktop.ui.Loader import org.briarproject.briar.desktop.viewmodel.viewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun ConversationScreen( contactId: ContactId, @@ -55,6 +57,7 @@ fun ConversationScreen( val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } + val (deleteAllMessagesDialogVisible, setDeleteAllMessagesDialog) = remember { mutableStateOf(false) } val scrollState = rememberLazyListState() BoxWithConstraints(Modifier.fillMaxSize()) { @@ -65,6 +68,9 @@ fun ConversationScreen( contactItem, onMakeIntroduction = { setInfoDrawer(true) + }, + onDeleteAllMessages = { + setDeleteAllMessagesDialog(true) } ) }, @@ -139,5 +145,16 @@ fun ConversationScreen( ) } } + + DeleteAllMessagesConfirmationDialog( + isVisible = deleteAllMessagesDialogVisible, + close = { setDeleteAllMessagesDialog(false) }, + onDelete = viewModel::deleteAllMessages + ) + + DeleteAllMessagesFailedDialog( + deletionResult = viewModel.deletionResult.value, + close = viewModel::confirmDeletionResult + ) } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt index bef737f175fa25ec663d0e77151b710072d8848d..f716ff85ca8d57db78ecdfeffd69ecbc4473a03a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -28,6 +28,7 @@ import org.briarproject.briar.api.attachment.AttachmentReader import org.briarproject.briar.api.autodelete.UnexpectedTimerException import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.api.conversation.DeletionResult import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent import org.briarproject.briar.api.identity.AuthorManager import org.briarproject.briar.api.introduction.IntroductionManager @@ -75,11 +76,15 @@ constructor( private val _newMessage = mutableStateOf("") + private val _deletionResult = mutableStateOf<DeletionResult?>(null) + val contactItem = _contactItem.asState() val messages = _messages.asList() val newMessage = _newMessage.asState() + val deletionResult = _deletionResult.asState() + fun setContactId(id: ContactId) { if (_contactId.value == id) return @@ -299,4 +304,22 @@ constructor( loadMessages(txn, contactItem.value!!) } } + + fun deleteAllMessages() = runOnDbThread { + try { + val result = conversationManager.deleteAllMessages(_contactId.value!!) + reloadConversationAfterDeletingMessages(result) + } finally { + // todo: + } + } + + private fun reloadConversationAfterDeletingMessages(result: DeletionResult) { + reloadMessages() + _deletionResult.value = if (!result.allDeleted()) result else null + } + + fun confirmDeletionResult() { + _deletionResult.value = null + } } diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties index 179f08dca8edc95debaa133a30f8dbb0fbabdcd1..32c74cea6aca208b62d6eacb3ed1e991ebb6166f 100644 --- a/src/main/resources/strings/BriarDesktop.properties +++ b/src/main/resources/strings/BriarDesktop.properties @@ -29,6 +29,15 @@ contacts.search.title=Contacts # Conversation conversation.message.new=New Message +conversation.delete.all.dialog.title=Confirm Message Deletion +conversation.delete.all.dialog.message=Are you sure that you want to delete all messages? +conversation.delete.failed.dialog.title=Could not delete all messages +conversation.delete.failed.dialog.message.ongoing_both=Messages related to ongoing invitations and introductions cannot be deleted until they conclude. +conversation.delete.failed.dialog.message.ongoing_introductions=Messages related to ongoing introductions cannot be deleted until they conclude. +conversation.delete.failed.dialog.message.ongoing_invitations=Messages related to ongoing invitations cannot be deleted until they conclude. +conversation.delete.failed.dialog.message.not_all_selected_both=To delete an invitation or introduction, you need to select the request and the response. +conversation.delete.failed.dialog.message.not_all_selected_introductions=To delete an introduction, you need to select the request and the response. +conversation.delete.failed.dialog.message.not_all_selected_invitations=To delete an invitation, you need to select the request and the response. # Private Groups groups.card.created=Created by {0} @@ -92,6 +101,9 @@ main.help.tor.port.socks=Tor Socks Port main.help.tor.port.control=Tor Control Port # Miscellaneous +cancel=Cancel +delete=Delete +ok=OK password=Password accept=Accept decline=Decline