diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index afc949ceae..7254a1fb7b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -26,6 +26,8 @@ import chat.simplex.app.views.chat.ComposeState import chat.simplex.app.views.helpers.* import kotlinx.datetime.Clock +// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) + @Composable fun ChatItemView( cInfo: ChatInfo, @@ -46,7 +48,11 @@ fun ChatItemView( val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart val showMenu = remember { mutableStateOf(false) } + val revealed = remember { mutableStateOf(false) } + val fullDeleteAllowed = remember(cInfo) { cInfo.fullDeletionAllowed } val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file) + val onLinkLongClick = { _: String -> showMenu.value = true } + Box( modifier = Modifier .padding(bottom = 4.dp) @@ -69,29 +75,36 @@ fun ChatItemView( .clip(RoundedCornerShape(18.dp)) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick), ) { - @Composable fun ContentItem() { - val mc = cItem.content.msgContent - val onLinkLongClick = { _: String -> showMenu.value = true } - if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem) - } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, longClick = { onLinkLongClick("") }) + @Composable + fun framedItemView() { + FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + } + + fun deleteMessageQuestionText(): String { + return if (fullDeleteAllowed) { + generalGetString(R.string.delete_message_cannot_be_undone_warning) } else { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + generalGetString(R.string.delete_message_mark_deleted_warning) } + } + + @Composable + fun MsgContentItemDropdownMenu() { DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, Modifier.width(220.dp) ) { - ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } - showMenu.value = false - }) + if (!cItem.meta.itemDeleted) { + ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + } + showMenu.value = false + }) + } ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = { val filePath = getLoadedFilePath(SimplexApp.context, cItem.file) when { @@ -124,15 +137,59 @@ fun ChatItemView( showMenu.value = false }) } + if (cItem.meta.itemDeleted && revealed.value) { + ItemAction( + stringResource(R.string.hide_verb), + Icons.Outlined.VisibilityOff, + onClick = { + revealed.value = false + showMenu.value = false + } + ) + } + DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) + } + } + + @Composable + fun MarkedDeletedItemDropdownMenu() { + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false }, + Modifier.width(220.dp) + ) { ItemAction( - stringResource(R.string.delete_verb), - Icons.Outlined.Delete, + stringResource(R.string.reveal_verb), + Icons.Outlined.Visibility, onClick = { + revealed.value = true showMenu.value = false - deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage) - }, - color = Color.Red + } ) + DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) + } + } + + @Composable + fun ContentItem() { + val mc = cItem.content.msgContent + if (cItem.meta.itemDeleted && !revealed.value) { + MarkedDeletedItemView(cItem, showMember = showMember) + MarkedDeletedItemDropdownMenu() + } else if (cItem.quotedItem == null && !cItem.meta.itemDeleted) { + if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { + EmojiItemView(cItem) + MsgContentItemDropdownMenu() + } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, longClick = { onLinkLongClick("") }) + MsgContentItemDropdownMenu() + } else { + framedItemView() + MsgContentItemDropdownMenu() + } + } else { + framedItemView() + MsgContentItemDropdownMenu() } } @@ -143,15 +200,7 @@ fun ChatItemView( onDismissRequest = { showMenu.value = false }, Modifier.width(220.dp) ) { - ItemAction( - stringResource(R.string.delete_verb), - Icons.Outlined.Delete, - onClick = { - showMenu.value = false - deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage) - }, - color = Color.Red - ) + DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) } } @@ -184,6 +233,24 @@ fun ChatItemView( } } +@Composable +fun DeleteItemAction( + cItem: ChatItem, + showMenu: MutableState, + questionText: String, + deleteMessage: (Long, CIDeleteMode) -> Unit +) { + ItemAction( + stringResource(R.string.delete_verb), + Icons.Outlined.Delete, + onClick = { + showMenu.value = false + deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage) + }, + color = Color.Red + ) +} + @Composable fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) { DropdownMenuItem(onClick) { @@ -201,10 +268,10 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo } } -fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteMode) -> Unit) { +fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { AlertManager.shared.showAlertDialogButtons( title = generalGetString(R.string.delete_message__question), - text = generalGetString(R.string.delete_message_cannot_be_undone_warning), + text = questionText, buttons = { Row( Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt index 945e59c115..9c40e381f0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Delete import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -16,15 +17,15 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.* import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.* import androidx.compose.ui.util.fastMap import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ChatItemLinkView -import chat.simplex.app.views.helpers.base64ToBitmap +import chat.simplex.app.views.helpers.* import kotlinx.datetime.Clock val SentColorLight = Color(0x1E45B8FF) @@ -65,6 +66,33 @@ fun FramedItemView( } } + @Composable + fun ciDeletedView() { + Row( + Modifier + .background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) + .fillMaxWidth() + .padding(start = 8.dp) + .padding(end = 12.dp) + .padding(top = 6.dp) + .padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + Icons.Outlined.Delete, + stringResource(R.string.marked_deleted_description), + Modifier.size(18.dp), + tint = if (isInDarkTheme()) FileDark else FileLight + ) + Text( + buildAnnotatedString { + withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) } + }, + style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), + ) + } + } + @Composable fun ciQuoteView(qi: CIQuote) { Row( @@ -130,6 +158,7 @@ fun FramedItemView( Box(contentAlignment = Alignment.BottomEnd) { Column(Modifier.width(IntrinsicSize.Max)) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { + if (ci.meta.itemDeleted) { ciDeletedView() } ci.quotedItem?.let { ciQuoteView(it) } if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt new file mode 100644 index 0000000000..677c269e35 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt @@ -0,0 +1,57 @@ +package chat.simplex.app.views.chat.item + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.ChatItem +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.generalGetString + +@Composable +fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) { + Surface( + shape = RoundedCornerShape(18.dp), + color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight, + ) { + Row( + Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + buildAnnotatedString { + // appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size + withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) } + }, + style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), + modifier = Modifier.padding(end = 8.dp) + ) + CIMetaView(ci) + } + } +} + +@Preview(showBackground = true) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + name = "Dark Mode" +) +@Composable +fun PreviewMarkedDeletedItemView() { + SimpleXTheme { + DeletedItemView( + ChatItem.getSampleData(itemDeleted = true) + ) + } +} diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index a1d089af54..ace8b2b070 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -168,10 +168,13 @@ Speichern Bearbeiten Löschen + ***Reveal + ***Hide Erlauben Die Nachricht löschen? Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden! - Nur für mich + ***Message will be marked for deletion. The recipient(s) will be able to reveal this message. + ***Delete for me Für alle diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 0a9f447392..dce10565f3 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -168,10 +168,13 @@ Сохранить Редактировать Удалить + Показать + Спрятать Разрешить Удалить сообщение? Сообщение будет удалено – это действие нельзя отменить! - Только для меня + Сообщение будет помечено на удаление. Получатель(и) сможет(смогут) посмотреть это сообщение. + Удалить для меня Для всех diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index ea7196a5e8..a420a74882 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -168,10 +168,13 @@ Save Edit Delete + Reveal + Hide Allow Delete message? Message will be deleted - this cannot be undone! - For me only + Message will be marked for deletion. The recipient(s) will be able to reveal this message. + Delete for me For everyone diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index c748ed4df6..d9bf9b1161 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -17,7 +17,7 @@ struct MarkedDeletedItemView: View { var body: some View { HStack(alignment: .bottom, spacing: 0) { if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") + Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption) } Text("marked deleted") .font(.caption)