diff --git a/src/main/kotlin/androidx/compose/material/DialogButton.kt b/src/main/kotlin/androidx/compose/material/DialogButton.kt new file mode 100644 index 0000000000000000000000000000000000000000..e16456ff831789a851088a66f2f2d18a523ccef6 --- /dev/null +++ b/src/main/kotlin/androidx/compose/material/DialogButton.kt @@ -0,0 +1,48 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package androidx.compose.material + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.sp +import org.briarproject.briar.desktop.theme.buttonTextNegative +import org.briarproject.briar.desktop.theme.buttonTextPositive +import java.util.Locale + +enum class ButtonType { + POSITIVE, + NEGATIVE, +} + +@Composable +fun DialogButton( + onClick: () -> Unit, + text: String, + type: ButtonType, +) { + TextButton(onClick = onClick) { + Text( + text.uppercase(Locale.getDefault()), + fontSize = 16.sp, + color = when (type) { + ButtonType.POSITIVE -> MaterialTheme.colors.buttonTextPositive + ButtonType.NEGATIVE -> MaterialTheme.colors.buttonTextNegative + }, + ) + } +} 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 a854d7ea11cd8e106e12f9b1b281b6bceb490c30..2f204c250386820d620528aaf815167c38bc5e06 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt @@ -21,9 +21,11 @@ package org.briarproject.briar.desktop.contact import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon +import androidx.compose.material.MenuDefaults import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowRight @@ -34,6 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.getCoreFeatureFlags @@ -45,6 +48,7 @@ fun ContactDropDown( close: () -> Unit, onMakeIntroduction: () -> Unit, onDeleteAllMessages: () -> Unit, + onChangeAlias: () -> Unit, onDeleteContact: () -> Unit, ) { val coreFeatureFlags = getCoreFeatureFlags() @@ -116,13 +120,14 @@ fun ContactDropDown( expanded = contactMode, onDismissRequest = { contactMode = false }, ) { - DropdownMenuItem(onClick = { false }) { - Text(i18n("contacts.dropdown.contact.title"), fontSize = 12.sp) - } - DropdownMenuItem(onClick = { false }, enabled = false) { + Text( + i18n("contacts.dropdown.contact.title"), fontSize = 12.sp, + modifier = Modifier.padding(MenuDefaults.DropdownMenuItemContentPadding).padding(vertical = 8.dp) + ) + DropdownMenuItem(onClick = { contactMode = false; onChangeAlias() }) { Text(i18n("contacts.dropdown.contact.change"), fontSize = 14.sp) } - DropdownMenuItem(onClick = { close(); onDeleteContact() }) { + DropdownMenuItem(onClick = { contactMode = false; onDeleteContact() }) { Text(i18n("contacts.dropdown.contact.delete"), fontSize = 14.sp) } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt index cb6b3f19a259e7d4e4b7e5a08e5861ba17d62795..4e22dac458691bb3420d6ff128e35cacd101f06e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -26,12 +26,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material.AlertDialog -import androidx.compose.material.Button +import androidx.compose.material.ButtonType.NEGATIVE +import androidx.compose.material.ButtonType.POSITIVE +import androidx.compose.material.DialogButton import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error @@ -154,14 +154,14 @@ fun AddContactDialog( Column(modifier = Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) { Text( - text = "Add Contact at a Distance", + text = i18n("conversation.add.contact.dialog.title"), fontSize = 24.sp, modifier = Modifier.align(Alignment.CenterVertically) ) } Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Text( - "Contact's Link", + i18n("conversation.add.contact.dialog.contact_link"), Modifier.width(128.dp).align(Alignment.CenterVertically), ) TextField( @@ -172,7 +172,7 @@ fun AddContactDialog( } Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Text( - "Contact's Name", + i18n("conversation.add.contact.dialog.contact_name"), Modifier.width(128.dp).align(Alignment.CenterVertically), ) TextField( @@ -183,7 +183,7 @@ fun AddContactDialog( } Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Text( - "Your Link", + i18n("conversation.add.contact.dialog.own_link"), modifier = Modifier.width(128.dp).align(Alignment.CenterVertically), ) TextField( @@ -194,22 +194,8 @@ fun AddContactDialog( } } }, - confirmButton = { - Button( - onClick = { - onSubmitAddContactDialog() - } - ) { - Text("Add") - } - }, - dismissButton = { - TextButton( - onClick = onClose - ) { - Text("Cancel", color = MaterialTheme.colors.onSurface) - } - }, + confirmButton = { DialogButton(onClick = onSubmitAddContactDialog, text = i18n("add"), type = POSITIVE) }, + dismissButton = { DialogButton(onClick = onClose, text = i18n("cancel"), type = NEGATIVE) }, ) if (error != null) { val (type, title, message) = errorMessage(error) @@ -219,11 +205,7 @@ fun AddContactDialog( } AlertDialog( onDismissRequest = onErrorDialogDismissed, - confirmButton = { - TextButton(onErrorDialogDismissed) { - Text(i18n("ok")) - } - }, + confirmButton = { DialogButton(onClick = onErrorDialogDismissed, text = i18n("ok"), type = POSITIVE) }, modifier = Modifier.widthIn(min = DIALOG_WIDTH), title = { Row( diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt index d18b95e4fcec36dab15fa7dee441951629c1e430..3ad127a00a683bdf5f6f02ec6bdf3f6c6b6a04b2 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationDialogs.kt @@ -20,12 +20,16 @@ package org.briarproject.briar.desktop.conversation import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.ButtonType.NEGATIVE +import androidx.compose.material.ButtonType.POSITIVE +import androidx.compose.material.DialogButton import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text -import androidx.compose.material.TextButton +import androidx.compose.material.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -33,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import org.briarproject.briar.api.conversation.DeletionResult import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview @@ -46,6 +51,7 @@ fun main() = preview( ) { var confirmationDialog by remember { mutableStateOf(false) } var failedDialog by remember { mutableStateOf(false) } + var changeAliasDialog by remember { mutableStateOf(false) } val deletionResult by derivedStateOf { if (!failedDialog) null else DeletionResult().apply { @@ -64,6 +70,10 @@ fun main() = preview( Button(onClick = { failedDialog = true }) { Text("Show deletion failed dialog") } + + Button(onClick = { changeAliasDialog = true }) { + Text("Show change alias dialog") + } } DeleteAllMessagesConfirmationDialog( @@ -72,6 +82,14 @@ fun main() = preview( ) DeleteAllMessagesFailedDialog(deletionResult) { failedDialog = false } + + val (alias, setAlias) = remember { mutableStateOf("Alice") } + ChangeAliasDialog( + isVisible = changeAliasDialog, + alias = alias, + setAlias = setAlias, + close = { changeAliasDialog = false }, + ) } @OptIn(ExperimentalMaterialApi::class) @@ -95,16 +113,8 @@ fun DeleteAllMessagesConfirmationDialog( text = { Text(i18n("conversation.delete.all.dialog.message")) }, - dismissButton = { - TextButton(onClick = { close(); onDelete() }) { - Text(i18n("delete")) - } - }, - confirmButton = { - TextButton(onClick = { close(); onCancel() }) { - Text(i18n("cancel")) - } - } + dismissButton = { DialogButton(onClick = { close(); onCancel() }, text = i18n("cancel"), type = NEGATIVE) }, + confirmButton = { DialogButton(onClick = { close(); onDelete() }, text = i18n("delete"), type = POSITIVE) }, ) } @@ -150,11 +160,35 @@ fun DeleteAllMessagesFailedDialog( text = { Text(message) }, - confirmButton = { - TextButton(onClick = close) { - Text(i18n("ok")) - } - } + confirmButton = { DialogButton(onClick = close, text = i18n("ok"), type = POSITIVE) }, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ChangeAliasDialog( + isVisible: Boolean, + alias: String, + setAlias: (String) -> Unit, + close: () -> Unit, + onConfirm: () -> Unit = {}, + onCancel: () -> Unit = {}, +) { + if (!isVisible) return + + AlertDialog( + onDismissRequest = { close; onCancel() }, + title = { + Text( + text = i18n("conversation.change.alias.dialog.title"), + modifier = Modifier.width(IntrinsicSize.Max).padding(vertical = 16.dp) + ) + }, + text = { + TextField(alias, setAlias) + }, + dismissButton = { DialogButton(onClick = { close(); onCancel() }, text = i18n("cancel"), type = NEGATIVE) }, + confirmButton = { DialogButton(onClick = { close(); onConfirm() }, text = i18n("change"), type = POSITIVE) }, ) } @@ -179,15 +213,7 @@ fun DeleteContactConfirmationDialog( text = { Text(i18n("conversation.delete.contact.dialog.message")) }, - dismissButton = { - TextButton(onClick = { close(); onDelete() }) { - Text(i18n("delete")) - } - }, - confirmButton = { - TextButton(onClick = { close(); onCancel() }) { - Text(i18n("cancel")) - } - } + dismissButton = { DialogButton(onClick = { close(); onCancel() }, text = i18n("cancel"), type = NEGATIVE) }, + confirmButton = { DialogButton(onClick = { close(); onDelete() }, text = i18n("delete"), type = POSITIVE) }, ) } 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 9100b8bdbec34196b7825f248e56c7aa6eb76029..85c4902ce69eafa15ce6571e22a66be2a6b97e8b 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt @@ -56,6 +56,7 @@ fun ConversationHeader( contactItem: ContactItem, onMakeIntroduction: () -> Unit, onDeleteAllMessages: () -> Unit, + onChangeAlias: () -> Unit, onDeleteContact: () -> Unit, ) { val (isExpanded, setExpanded) = remember { mutableStateOf(false) } @@ -101,6 +102,7 @@ fun ConversationHeader( { setExpanded(false) }, onMakeIntroduction, onDeleteAllMessages, + onChangeAlias, onDeleteContact ) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt index 65883608235dbdf46f14c130dcc1ef08019a2d5c..6ffd124281234d51de7edd3d7eed98c90d540cd0 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt @@ -27,9 +27,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.ButtonType.NEGATIVE +import androidx.compose.material.ButtonType.POSITIVE +import androidx.compose.material.DialogButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,8 +43,6 @@ import org.briarproject.bramble.api.sync.GroupId import org.briarproject.bramble.api.sync.MessageId import org.briarproject.briar.api.client.SessionId import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.INTRODUCTION -import org.briarproject.briar.desktop.theme.buttonTextNegative -import org.briarproject.briar.desktop.theme.buttonTextPositive import org.briarproject.briar.desktop.theme.noticeIn import org.briarproject.briar.desktop.theme.noticeOut import org.briarproject.briar.desktop.theme.privateMessageDate @@ -122,28 +122,10 @@ fun ConversationRequestItemView( Row(modifier = Modifier.align(statusAlignment)) { if (!m.answered) { - TextButton({ onResponse(false) }) { - Text( - i18n("decline").uppercase(), - fontSize = 16.sp, - color = MaterialTheme.colors.buttonTextNegative - ) - } - TextButton({ onResponse(true) }) { - Text( - i18n("accept").uppercase(), - fontSize = 16.sp, - color = MaterialTheme.colors.buttonTextPositive - ) - } + DialogButton(onClick = { onResponse(false) }, text = i18n("decline"), type = NEGATIVE) + DialogButton(onClick = { onResponse(false) }, text = i18n("accept"), type = POSITIVE) } else if (m.canBeOpened) { - TextButton(onOpenRequestedShareable) { - Text( - i18n("open").uppercase(), - fontSize = 16.sp, - color = MaterialTheme.colors.buttonTextPositive - ) - } + DialogButton(onClick = onOpenRequestedShareable, text = i18n("open"), type = POSITIVE) } else { Spacer(Modifier.height(8.dp)) } 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 31964e446bc45c26daea97a303d53c8b1179bf0f..89abe704c9066450494cfac6b810d00a0485403a 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -70,6 +70,7 @@ fun ConversationScreen( val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } val (deleteAllMessagesDialogVisible, setDeleteAllMessagesDialog) = remember { mutableStateOf(false) } + val (changeAliasDialogVisible, setChangeAliasDialog) = remember { mutableStateOf(false) } val (deleteContactDialogVisible, setDeleteContactDialog) = remember { mutableStateOf(false) } BoxWithConstraints(Modifier.fillMaxSize()) { @@ -84,6 +85,9 @@ fun ConversationScreen( onDeleteAllMessages = { setDeleteAllMessagesDialog(true) }, + onChangeAlias = { + setChangeAliasDialog(true) + }, onDeleteContact = { setDeleteContactDialog(true) } @@ -159,6 +163,15 @@ fun ConversationScreen( close = viewModel::confirmDeletionResult ) + ChangeAliasDialog( + isVisible = changeAliasDialogVisible, + close = { setChangeAliasDialog(false) }, + onConfirm = viewModel::changeAlias, + onCancel = viewModel::resetAlias, + alias = viewModel.newAlias.value, + setAlias = viewModel::setNewAlias, + ) + DeleteContactConfirmationDialog( isVisible = deleteContactDialogVisible, close = { setDeleteContactDialog(false) }, 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 33ed0e2adb9fe7afcd31b9bb7ea904df49eb0904..543fc940ff0562d3a10cb76c69b25b2a6cccf16e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -118,6 +118,10 @@ constructor( val deletionResult = _deletionResult.asState() + // for update alias dialog + private val _newAlias = mutableStateOf("") + val newAlias = _newAlias.asState() + fun setContactId(id: ContactId) { if (_contactId.value == id) return @@ -143,6 +147,10 @@ constructor( _newMessageImage.value = image } + fun setNewAlias(alias: String) { + _newAlias.value = alias + } + fun sendMessage() { val text = _newMessage.value val image = _newMessageImage.value @@ -278,7 +286,10 @@ constructor( loadAvatar(authorManager, attachmentReader, txn, contact), ) LOG.logDuration("Loading contact", start) - txn.attach { _contactItem.value = contactItem } + txn.attach { + _contactItem.value = contactItem + _newAlias.value = contactItem.alias ?: "" + } return contactItem } catch (e: NoSuchContactException) { // todo: handle this properly @@ -441,6 +452,19 @@ constructor( _deletionResult.value = null } + fun changeAlias() = runOnDbThread { + val newAlias = _newAlias.value.ifBlank { null } + if (_contactId.value != null && contactItem.value != null) { + contactManager.setContactAlias(_contactId.value!!, newAlias) + _contactItem.value = contactItem.value!!.updateAlias(newAlias) + } + } + + fun resetAlias() { + println(contactItem.value) + _newAlias.value = contactItem.value?.alias ?: "" + } + fun deleteContact() = runOnDbThread { contactManager.removeContact(_contactId.value!!) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt index 540cc31fa74e1750ae9ebf20accca96e6bb60439..4d9b4cea0d7442cf25d431bdfeefdea8b96e7ba9 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt @@ -22,6 +22,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.AlertDialog +import androidx.compose.material.ButtonType.NEGATIVE +import androidx.compose.material.ButtonType.POSITIVE +import androidx.compose.material.DialogButton import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text @@ -85,9 +88,11 @@ fun LoginScreen( title = { Text(i18n("startup.error.decryption.title")) }, text = { Text(i18n("startup.error.decryption.text")) }, confirmButton = { - TextButton(onClick = viewHolder::closeDecryptionFailedDialog) { - Text(i18n("ok")) - } + DialogButton( + onClick = viewHolder::closeDecryptionFailedDialog, + text = i18n("ok"), + type = POSITIVE + ) }, ) } @@ -127,15 +132,11 @@ fun LoginForm( onDismissRequest = closeDialog, title = { Text(i18n("startup.password_forgotten.title")) }, text = { Text(i18n("startup.password_forgotten.text")) }, - confirmButton = { - TextButton(onClick = closeDialog) { - Text(i18n("cancel")) - } - }, dismissButton = { - TextButton(onClick = { deleteAccount(); closeDialog() }) { - Text(i18n("delete")) - } + DialogButton(onClick = closeDialog, text = i18n("cancel"), type = NEGATIVE) + }, + confirmButton = { + DialogButton(onClick = { deleteAccount(); closeDialog() }, text = i18n("delete"), type = POSITIVE) }, modifier = Modifier.width(500.dp) ) diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties index 67245e13eda6cc0c3c2a02fcb02eaa4c21ae0c42..654fe8946da347be18c4b6983fbf006c658f92aa 100644 --- a/src/main/resources/strings/BriarDesktop.properties +++ b/src/main/resources/strings/BriarDesktop.properties @@ -47,8 +47,13 @@ conversation.delete.failed.dialog.message.ongoing_invitations=Messages related t 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. +conversation.add.contact.dialog.title=Add Contact at a Distance +conversation.add.contact.dialog.own_link=Your Link +conversation.add.contact.dialog.contact_link=Contact's Link +conversation.add.contact.dialog.contact_name=Contact's Name conversation.delete.contact.dialog.title=Confirm Contact Deletion conversation.delete.contact.dialog.message=Are you sure that you want to remove this contact and all messages exchanged with this contact? +conversation.change.alias.dialog.title=Change contact name # Private Groups groups.card.created=Created by {0} @@ -134,6 +139,8 @@ cancel=Cancel delete=Delete ok=OK accept=Accept +add=Add +change=Change decline=Decline back=Back next=Next