From fac6e42c228ec18a6246debcc9a29f7aa56eb2d3 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 13 Jul 2025 10:39:51 +0200 Subject: [PATCH] Added edit/retract message features --- CHANGELOG.md | 1 + .../notifications/NotificationsManager.kt | 57 +++++++++ .../chat/fragment/ConversationFragment.kt | 59 +++++++++- .../ui/main/chat/model/ConversationModel.kt | 22 +++- .../ui/main/chat/model/EventLogModel.kt | 1 - .../chat/model/MessageDeleteDialogModel.kt | 35 ++++++ .../ui/main/chat/model/MessageModel.kt | 27 +++++ .../ChatMessageLongPressViewModel.kt | 20 ++++ .../chat/viewmodel/ConversationViewModel.kt | 33 ++++++ .../SendMessageInConversationViewModel.kt | 33 ++++++ .../java/org/linphone/utils/DialogUtils.kt | 18 +++ .../java/org/linphone/utils/LinphoneUtils.kt | 10 ++ app/src/main/res/drawable/trash.xml | 9 ++ .../main/res/layout/chat_bubble_incoming.xml | 27 ++++- .../layout/chat_bubble_long_press_menu.xml | 17 +++ .../main/res/layout/chat_bubble_outgoing.xml | 27 ++++- .../layout/chat_conversation_edit_area.xml | 69 +++++++++++ .../layout/chat_conversation_send_area.xml | 11 +- .../res/layout/dialog_delete_chat_message.xml | 109 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 9 ++ app/src/main/res/values/strings.xml | 11 +- 21 files changed, 596 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt create mode 100644 app/src/main/res/drawable/trash.xml create mode 100644 app/src/main/res/layout/chat_conversation_edit_area.xml create mode 100644 app/src/main/res/layout/dialog_delete_chat_message.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be5d7f46..c22e64631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Group changes to describe their impact on the project, as follows: ## [6.1.0] - Unreleased ### Added +- Added the ability to edit/delete chat messages sent less than 24 hours ago. - Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Support right click on some items to open bottom sheet/menu - Added toggle speaker action in active call notification diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index f20d339cd..5030cd380 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -481,6 +481,18 @@ class NotificationsManager } } } + + @WorkerThread + override fun onMessageRetracted(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + Log.i("$TAG A message has been retracted, checking if notification should be updated") + updateConversationNotification(chatRoom, message) + } + + @WorkerThread + override fun onMessageContentEdited(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + Log.i("$TAG A message has been edited, checking if notification should be updated") + updateConversationNotification(chatRoom, message) + } } val chatMessageListener: ChatMessageListener = object : ChatMessageListenerStub() { @@ -1100,6 +1112,7 @@ class NotificationsManager val notifiableMessage = NotifiableMessage( text, + message.messageId, contact, displayName, address.asStringUriOnly(), @@ -1262,6 +1275,7 @@ class NotificationsManager val address = message.fromAddress val notifiableMessage = NotifiableMessage( text, + message.messageId, contact, displayName, address.asStringUriOnly(), @@ -1637,6 +1651,7 @@ class NotificationsManager val senderAddress = message.fromAddress val reply = NotifiableMessage( text, + message.messageId, null, notifiable.myself ?: LinphoneUtils.getDisplayName(senderAddress), senderAddress.asStringUriOnly(), @@ -1929,6 +1944,47 @@ class NotificationsManager } } + @WorkerThread + private fun updateConversationNotification(chatRoom: ChatRoom, message: ChatMessage) { + if (corePreferences.disableChat) return + + val chatRoomPeerAddress = chatRoom.peerAddress.asStringUriOnly() + val notifiable: Notifiable? = chatNotificationsMap[chatRoomPeerAddress] + if (notifiable == null) { + Log.i("$TAG No notification for conversation [$chatRoomPeerAddress], nothing to do") + return + } + + val found = notifiable.messages.find { + it.messageId == message.messageId + } + if (found != null) { + Log.i("$TAG Edited message is in the currently displayed notification, updating it") + val index = notifiable.messages.indexOf(found) + notifiable.messages.remove(found) + if (!message.isRetracted) { + val notifiableMessage = getNotifiableForChatMessage(message) + notifiable.messages.add(index, notifiableMessage) + } + chatNotificationsMap[chatRoomPeerAddress] = notifiable + + val me = coreContext.contactsManager.getMePerson(chatRoom.localAddress) + val pendingIntent = getChatRoomPendingIntent( + chatRoom, + notifiable.notificationId + ) + val notification = createMessageNotification( + notifiable, + pendingIntent, + LinphoneUtils.getConversationId(chatRoom), + me + ) + notify(notifiable.notificationId, notification, CHAT_TAG) + } else { + Log.i("$TAG Edited/retracted message isn't in currently displayed notification, nothing to update") + } + } + @AnyThread fun foregroundServiceTypeMaskToString(mask: Int): String { var stringBuilder = StringBuilder() @@ -1962,6 +2018,7 @@ class NotificationsManager class NotifiableMessage( val message: String, + val messageId: String, var friend: Friend?, var sender: String, val senderAddress: String, diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index ceb405424..f05b0a78a 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -94,6 +94,7 @@ import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.showKeyboard import androidx.core.net.toUri +import org.linphone.ui.main.chat.model.MessageDeleteDialogModel @UiThread open class ConversationFragment : SlidingPaneChildFragment() { @@ -846,6 +847,22 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } + messageLongPressViewModel.editMessageEvent.observe(viewLifecycleOwner) { + it.consume { + val model = messageLongPressViewModel.messageModel.value + if (model != null) { + sendMessageViewModel.editMessage(model) + + // Open keyboard & focus edit text + binding.sendArea.messageToSend.showKeyboard() + // Put cursor at the end + coreContext.postOnMainThread { + binding.sendArea.messageToSend.setSelection(binding.sendArea.messageToSend.length()) + } + } + } + } + messageLongPressViewModel.replyToMessageEvent.observe(viewLifecycleOwner) { it.consume { val model = messageLongPressViewModel.messageModel.value @@ -861,7 +878,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { it.consume { val model = messageLongPressViewModel.messageModel.value if (model != null) { - viewModel.deleteChatMessage(model) + showHowToDeleteMessageDialog(model) } } } @@ -1559,4 +1576,44 @@ open class ConversationFragment : SlidingPaneChildFragment() { Log.e("$TAG No activity found to handle intent ACTION_CREATE_DOCUMENT: $exception") } } + + private fun showHowToDeleteMessageDialog(model: MessageModel) { + val canBeRetracted = messageLongPressViewModel.canBeRemotelyDeleted.value == true + val dialogModel = MessageDeleteDialogModel(canBeRetracted) + + val dialog = DialogUtils.getHowToDeleteMessageDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.cancelEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.deleteLocallyEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Deleting chat message locally") + viewModel.deleteChatMessage(model) + dialog.dismiss() + } + } + + dialogModel.deleteForEveryoneEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Deleting chat message (content) for everyone") + viewModel.deleteChatMessageForEveryone(model) + dialog.dismiss() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 3ac6edc52..1ad3cb8e6 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -170,6 +170,22 @@ class ConversationModel Log.i("$TAG An ephemeral message lifetime has expired, updating last displayed message") updateLastMessage() } + + @WorkerThread + override fun onMessageRetracted(chatRoom: ChatRoom, message: ChatMessage) { + if (lastMessage == null || message.messageId == lastMessage?.messageId) { + Log.i("$TAG Last message [${message.messageId}] has been retracted") + updateLastMessage() + } + } + + @WorkerThread + override fun onMessageContentEdited(chatRoom: ChatRoom, message: ChatMessage) { + if (lastMessage == null || message.messageId == lastMessage?.messageId) { + Log.i("$TAG Last message [${message.messageId}] has been edited") + updateLastMessage() + } + } } private val chatMessageListener = object : ChatMessageListenerStub() { @@ -309,7 +325,9 @@ class ConversationModel lastMessageDeliveryIcon.postValue(LinphoneUtils.getChatIconResId(message.state)) } - if (message.isForward) { + if (message.isRetracted) { + lastMessageContentIcon.postValue(R.drawable.trash) + } else if (message.isForward) { lastMessageContentIcon.postValue(R.drawable.forward) } else { val firstContent = message.contents.firstOrNull() @@ -350,7 +368,7 @@ class ConversationModel if (message.isOutgoing && message.state != ChatMessage.State.Displayed) { message.addListener(chatMessageListener) - } else if (message.contents.find { it.isFileTransfer == true } != null) { + } else if (message.contents.find { it.isFileTransfer } != null) { message.addListener(chatMessageListener) } diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt index b0da4ae9d..cbe5f1fce 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt @@ -51,7 +51,6 @@ class EventLogModel EventModel(eventLog) } else { val chatMessage = eventLog.chatMessage!! - MessageModel( chatMessage, isFromGroup, diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt new file mode 100644 index 000000000..c4bf5b451 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt @@ -0,0 +1,35 @@ +package org.linphone.ui.main.chat.model + +import androidx.annotation.UiThread +import androidx.lifecycle.MutableLiveData +import org.linphone.utils.Event + +class MessageDeleteDialogModel(val canBeRetracted: Boolean) { + val dismissEvent = MutableLiveData>() + + val cancelEvent = MutableLiveData>() + + val deleteLocallyEvent = MutableLiveData>() + + val deleteForEveryoneEvent = MutableLiveData>() + + @UiThread + fun dismiss() { + dismissEvent.value = Event(true) + } + + @UiThread + fun cancel() { + cancelEvent.value = Event(true) + } + + @UiThread + fun deleteLocally() { + deleteLocallyEvent.value = Event(true) + } + + @UiThread + fun deleteForEveryone() { + deleteForEveryoneEvent.value = Event(true) + } +} diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 067954373..99cad6b9b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -145,6 +145,10 @@ class MessageModel val firstFileModel = MediatorLiveData() + val hasBeenEdited = MutableLiveData() + + val hasBeenRetracted = MutableLiveData() + val isSelected = MutableLiveData() // Below are for conferences info @@ -303,6 +307,21 @@ class MessageModel Log.d("$TAG Ephemeral timer started") updateEphemeralTimer() } + + @WorkerThread + override fun onContentEdited(message: ChatMessage) { + Log.i("$TAG Message [${message.messageId}] has been edited") + hasBeenEdited.postValue(true) + computeContentsList() + } + + @WorkerThread + override fun onRetracted(message: ChatMessage) { + Log.i("$TAG Content(s) of the message have been deleted by it's sender") + hasBeenEdited.postValue(false) + hasBeenRetracted.postValue(true) + computeContentsList() + } } init { @@ -320,6 +339,8 @@ class MessageModel statusIcon.postValue(LinphoneUtils.getChatIconResId(chatMessage.state)) updateReactionsList() + hasBeenEdited.postValue(chatMessage.isEdited && !chatMessage.isRetracted) + hasBeenRetracted.postValue(chatMessage.isRetracted) computeContentsList() if (chatMessage.isReply) { // Wait to see if original message is found before setting isReply to true @@ -421,6 +442,12 @@ class MessageModel text.postValue(Spannable.Factory.getInstance().newSpannable("")) filesList.value.orEmpty().forEach(FileModel::destroy) + if (chatMessage.isRetracted) { + meetingFound.postValue(false) + isVoiceRecord.postValue(false) + isTextEmoji.postValue(false) + } + var displayableContentFound = false var contentIndex = 0 val filesPath = arrayListOf() diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt index 32e65c0aa..b4ac78b38 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt @@ -48,6 +48,10 @@ class ChatMessageLongPressViewModel : GenericViewModel() { val isChatRoomReadOnly = MutableLiveData() + val canBeEdited = MutableLiveData() + + val canBeRemotelyDeleted = MutableLiveData() + val messageModel = MutableLiveData() val isMessageOutgoing = MutableLiveData() @@ -58,6 +62,10 @@ class ChatMessageLongPressViewModel : GenericViewModel() { MutableLiveData>() } + val editMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val replyToMessageEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -76,6 +84,8 @@ class ChatMessageLongPressViewModel : GenericViewModel() { init { visible.value = false + canBeEdited.value = false + canBeRemotelyDeleted.value = false } @UiThread @@ -87,6 +97,9 @@ class ChatMessageLongPressViewModel : GenericViewModel() { @UiThread fun setMessage(model: MessageModel) { + canBeEdited.postValue(model.chatMessage.isEditable) + canBeRemotelyDeleted.postValue(model.chatMessage.isRetractable) + hideCopyTextToClipboard.value = model.text.value.isNullOrEmpty() isChatRoomReadOnly.value = model.chatRoomIsReadOnly isMessageOutgoing.value = model.isOutgoing @@ -124,6 +137,13 @@ class ChatMessageLongPressViewModel : GenericViewModel() { dismiss() } + @UiThread + fun edit() { + Log.i("$TAG Editing message") + editMessageEvent.value = Event(true) + dismiss() + } + @UiThread fun copyClickListener() { Log.i("$TAG Copying message text into clipboard") diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index fb9572382..f69f74e2b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -305,6 +305,30 @@ class ConversationViewModel Log.e("$TAG Failed to find matching message in conversation events list") } } + + @WorkerThread + override fun onMessageRetracted(chatRoom: ChatRoom, message: ChatMessage) { + for (model in eventsList.reversed()) { + if (model.model is MessageModel && model.model.replyToMessageId == message.messageId) { + model.model.computeReplyInfo() + break + } + } + + if (message.isOutgoing) { + messageDeletedEvent.postValue(Event(true)) + } + } + + @WorkerThread + override fun onMessageContentEdited(chatRoom: ChatRoom, message: ChatMessage) { + for (model in eventsList.reversed()) { + if (model.model is MessageModel && model.model.replyToMessageId == message.messageId) { + model.model.computeReplyInfo() + break + } + } + } } private val contactsListener = object : ContactsManager.ContactsListener { @@ -455,6 +479,15 @@ class ConversationViewModel } } + @UiThread + fun deleteChatMessageForEveryone(chatMessageModel: MessageModel) { + coreContext.postOnCoreThread { + val message = chatMessageModel.chatMessage + Log.i("$TAG Sending order to delete contents of message [${message.messageId}] to every participant of the conversation") + chatRoom.retractMessage(message) + } + } + @UiThread fun markAsRead() { if (!isChatRoomInitialized()) return diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 7d3a64cf3..4d4750f2b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -84,6 +84,10 @@ class SendMessageInConversationViewModel val attachments = MutableLiveData>() + val isEditing = MutableLiveData() + + val isEditingMessage = MutableLiveData() + val isReplying = MutableLiveData() val isReplyingTo = MutableLiveData() @@ -133,6 +137,8 @@ class SendMessageInConversationViewModel private var chatMessageToReplyTo: ChatMessage? = null + private var chatMessageToEdit: ChatMessage? = null + private lateinit var voiceMessageRecorder: Recorder private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null @@ -228,6 +234,28 @@ class SendMessageInConversationViewModel areFilePickersOpen.value = false } + @UiThread + fun editMessage(model: MessageModel) { + val newValue = model.text.value?.toString() ?: "" + textToSend.value = newValue + + coreContext.postOnCoreThread { + val message = model.chatMessage + Log.i("$TAG Pending message edit [${message.messageId}]") + chatMessageToEdit = message + isEditingMessage.postValue(LinphoneUtils.getFormattedTextDescribingMessage(message)) + isEditing.postValue(true) + } + } + + @UiThread + fun cancelEdit() { + Log.i("$TAG Cancelling edit") + isEditing.value = false + chatMessageToEdit = null + textToSend.value = "" + } + @UiThread fun replyToMessage(model: MessageModel) { coreContext.postOnCoreThread { @@ -253,9 +281,12 @@ class SendMessageInConversationViewModel val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt()) val messageToReplyTo = chatMessageToReplyTo + val messageToEdit = chatMessageToEdit val message = if (messageToReplyTo != null) { Log.i("$TAG Sending message as reply to [${messageToReplyTo.messageId}]") chatRoom.createReplyMessage(messageToReplyTo) + } else if (messageToEdit != null) { + chatRoom.createReplacesMessage(messageToEdit) } else { chatRoom.createEmptyMessage() } @@ -325,6 +356,7 @@ class SendMessageInConversationViewModel Log.i("$TAG Message sent, re-setting defaults") textToSend.postValue("") isReplying.postValue(false) + isEditing.postValue(false) isFileAttachmentsListOpen.postValue(false) isParticipantsListOpen.postValue(false) isEmojiPickerOpen.postValue(false) @@ -339,6 +371,7 @@ class SendMessageInConversationViewModel attachments.postValue(attachmentsList) chatMessageToReplyTo = null + chatMessageToEdit = null maxNumberOfAttachmentsReached.postValue(false) } } diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 0b7b1bb87..a1f003e77 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -67,6 +67,8 @@ import org.linphone.ui.main.contacts.model.ContactTrustDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable +import org.linphone.databinding.DialogDeleteChatMessageBinding +import org.linphone.ui.main.chat.model.MessageDeleteDialogModel class DialogUtils { companion object { @@ -530,6 +532,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getHowToDeleteMessageDialog( + context: Context, + viewModel: MessageDeleteDialogModel + ): Dialog { + val binding: DialogDeleteChatMessageBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_delete_chat_message, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread private fun getDialog(context: Context, binding: ViewDataBinding): Dialog { val dialog = Dialog(context, R.style.Theme_LinphoneDialog) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 5a5a82fe9..2e6fe3fcf 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -607,6 +607,16 @@ class LinphoneUtils { @WorkerThread private fun getTextDescribingMessage(message: ChatMessage): Pair { + // Check if message is empty (when deleted by it's sender, for everyone) + if (message.isRetracted) { + val text = if (message.isOutgoing) { + AppUtils.getString(R.string.conversation_message_content_deleted_by_us_label) + } else { + AppUtils.getString(R.string.conversation_message_content_deleted_label) + } + return Pair(text, "") + } + // If message contains text, then use that var text = message.contents.find { content -> content.isText }?.utf8Text ?: "" var contentDescription = "" diff --git a/app/src/main/res/drawable/trash.xml b/app/src/main/res/drawable/trash.xml new file mode 100644 index 000000000..1a17130c7 --- /dev/null +++ b/app/src/main/res/drawable/trash.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index e77e728e0..c863aff71 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -235,7 +235,22 @@ android:textColor="?attr/color_main2_800" android:gravity="center_vertical|start" android:includeFontPadding="@{!model.isTextEmoji}" - android:visibility="@{model.text.length() > 0 ? View.VISIBLE : View.GONE}"/> + android:visibility="@{model.text.length() > 0 && !model.hasBeenRetracted ? View.VISIBLE : View.GONE}"/> + + + + + + + + + android:visibility="@{model.text.length() > 0 && !model.hasBeenRetracted ? View.VISIBLE : View.GONE}"/> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index 8e4e0610f..24a6c28bf 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -39,6 +39,13 @@ android:visibility="@{viewModel.isReplying ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@id/edit_area" /> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3c4e2a889..2add14319 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -548,6 +548,13 @@ Choisir un fichier Impossible d\'ouvrir le fichier ! Impossible d\'ouvrir un PDF protégé par mot de passe + Modification du message + Modifié + Supprimer le message ? + Pour moi + Pour tout le monde + Le message a été supprimé + Vous avez supprimé le message Participants (%s) Ajouter des participants @@ -805,6 +812,7 @@ Inviter Ré-envoyer Info de réception + Modifier Répondre Transférer Copier le texte @@ -913,6 +921,7 @@ Démarre l\'enregistrement d\'un message vocal Envoie le message Le message ne sera plus une réponse à un précédent message + Annule l\'édition du message Ouvre le selectionneur d\'emoji Ouvre le selectionneur de fichier Cliquez pour modifier le sujet de la conversation diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b32c97779..904b2d6c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,7 +28,7 @@ 😢 GNU General Public License v3.0 - © Belledonne Communications 2010-2024 + © Belledonne Communications 2010-2025 linphone-android@belledonne-communications.com https://linphone.org/contact @@ -591,6 +591,13 @@ Pick file File can\'t be opened! Can\'t open password protected PDFs yet + Message being edited + Edited + Delete this message? + For me + For everyone + This message has been deleted + You have deleted this message Group members (%s) Add participants @@ -848,6 +855,7 @@ Invite Re-send Delivery status + Edit Reply Forward Copy @@ -956,6 +964,7 @@ Starts recording a voice message Sends message in conversation Message will no longer be a reply to a previous message + Cancels message edition Opens emoji picker Opens file picker Click to edit the subject of this conversation