mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Added edit/retract message features
This commit is contained in:
parent
e8d3c8750a
commit
fac6e42c22
21 changed files with 596 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class EventLogModel
|
|||
EventModel(eventLog)
|
||||
} else {
|
||||
val chatMessage = eventLog.chatMessage!!
|
||||
|
||||
MessageModel(
|
||||
chatMessage,
|
||||
isFromGroup,
|
||||
|
|
|
|||
|
|
@ -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<Event<Boolean>>()
|
||||
|
||||
val cancelEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val deleteLocallyEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val deleteForEveryoneEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +145,10 @@ class MessageModel
|
|||
|
||||
val firstFileModel = MediatorLiveData<FileModel>()
|
||||
|
||||
val hasBeenEdited = MutableLiveData<Boolean>()
|
||||
|
||||
val hasBeenRetracted = MutableLiveData<Boolean>()
|
||||
|
||||
val isSelected = MutableLiveData<Boolean>()
|
||||
|
||||
// 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<FileModel>()
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ class ChatMessageLongPressViewModel : GenericViewModel() {
|
|||
|
||||
val isChatRoomReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
val canBeEdited = MutableLiveData<Boolean>()
|
||||
|
||||
val canBeRemotelyDeleted = MutableLiveData<Boolean>()
|
||||
|
||||
val messageModel = MutableLiveData<MessageModel>()
|
||||
|
||||
val isMessageOutgoing = MutableLiveData<Boolean>()
|
||||
|
|
@ -58,6 +62,10 @@ class ChatMessageLongPressViewModel : GenericViewModel() {
|
|||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val editMessageEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val replyToMessageEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ class SendMessageInConversationViewModel
|
|||
|
||||
val attachments = MutableLiveData<ArrayList<FileModel>>()
|
||||
|
||||
val isEditing = MutableLiveData<Boolean>()
|
||||
|
||||
val isEditingMessage = MutableLiveData<Spannable>()
|
||||
|
||||
val isReplying = MutableLiveData<Boolean>()
|
||||
|
||||
val isReplyingTo = MutableLiveData<String>()
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -607,6 +607,16 @@ class LinphoneUtils {
|
|||
|
||||
@WorkerThread
|
||||
private fun getTextDescribingMessage(message: ChatMessage): Pair<String, String> {
|
||||
// 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 = ""
|
||||
|
|
|
|||
9
app/src/main/res/drawable/trash.xml
Normal file
9
app/src/main/res/drawable/trash.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M216,48L176,48L176,40a24,24 0,0 0,-24 -24L104,16A24,24 0,0 0,80 40v8L40,48a8,8 0,0 0,0 16h8L48,208a16,16 0,0 0,16 16L192,224a16,16 0,0 0,16 -16L208,64h8a8,8 0,0 0,0 -16ZM96,40a8,8 0,0 1,8 -8h48a8,8 0,0 1,8 8v8L96,48ZM192,208L64,208L64,64L192,64ZM112,104v64a8,8 0,0 1,-16 0L96,104a8,8 0,0 1,16 0ZM160,104v64a8,8 0,0 1,-16 0L144,104a8,8 0,0 1,16 0Z"
|
||||
android:fillColor="#4e6074"/>
|
||||
</vector>
|
||||
|
|
@ -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}"/>
|
||||
|
||||
<org.linphone.ui.main.chat.view.ChatBubbleTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/retracted_content"
|
||||
android:onLongClick="@{onLongClickListener}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/conversation_message_content_deleted_label"
|
||||
android:textSize="@dimen/chat_bubble_text_size"
|
||||
android:textColor="?attr/color_main2_500"
|
||||
android:gravity="center_vertical|start"
|
||||
android:drawableEnd="@drawable/trash"
|
||||
android:drawablePadding="5dp"
|
||||
android:drawableTint="?attr/color_main2_500"
|
||||
android:visibility="@{model.hasBeenRetracted ? View.VISIBLE : View.GONE, default=gone}"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -254,6 +269,16 @@
|
|||
android:text="@{model.time, default=`13:40`}"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/edited"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:text="@string/conversation_message_edited_label"
|
||||
android:visibility="@{model.hasBeenEdited ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/delivery_status"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,23 @@
|
|||
android:background="?attr/color_separator"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/chat_bubble_popup_menu_style"
|
||||
android:id="@+id/action_edit"
|
||||
android:onClick="@{() -> viewModel.edit()}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/menu_edit_chat_message"
|
||||
android:background="@drawable/action_background_middle"
|
||||
android:visibility="@{viewModel.isChatRoomReadOnly || !viewModel.canBeEdited ? View.GONE : View.VISIBLE}"
|
||||
android:drawableEnd="@drawable/pencil_simple" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/color_separator"
|
||||
android:visibility="@{viewModel.isChatRoomReadOnly || !viewModel.canBeEdited ? View.GONE : View.VISIBLE}"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/chat_bubble_popup_menu_style"
|
||||
android:id="@+id/action_reply"
|
||||
|
|
|
|||
|
|
@ -207,7 +207,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}"/>
|
||||
|
||||
<org.linphone.ui.main.chat.view.ChatBubbleTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/retracted_content"
|
||||
android:onLongClick="@{onLongClickListener}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/conversation_message_content_deleted_by_us_label"
|
||||
android:textSize="@dimen/chat_bubble_text_size"
|
||||
android:textColor="?attr/color_main2_500"
|
||||
android:gravity="center_vertical|start"
|
||||
android:drawableEnd="@drawable/trash"
|
||||
android:drawablePadding="5dp"
|
||||
android:drawableTint="?attr/color_main2_500"
|
||||
android:visibility="@{model.hasBeenRetracted ? View.VISIBLE : View.GONE, default=gone}"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -238,6 +253,16 @@
|
|||
app:tint="?attr/color_main2_600"
|
||||
android:visibility="@{model.isEphemeral ? View.VISIBLE : View.GONE, default=gone}" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/edited"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:text="@string/conversation_message_edited_label"
|
||||
android:visibility="@{model.hasBeenEdited ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/date_time"
|
||||
|
|
|
|||
69
app/src/main/res/layout/chat_conversation_edit_area.xml
Normal file
69
app/src/main/res/layout/chat_conversation_edit_area.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/color_main2_000">
|
||||
|
||||
<View
|
||||
android:id="@+id/edit_separator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/color_separator"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cancel"
|
||||
android:onClick="@{() -> viewModel.cancelEdit()}"
|
||||
android:layout_width="@dimen/icon_size"
|
||||
android:layout_height="@dimen/icon_size"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:src="@drawable/x"
|
||||
android:contentDescription="@string/content_description_chat_cancel_edit"
|
||||
app:tint="@color/icon_color_selector"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/edit_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/conversation_editing_message_title"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/color_main2_500"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_300"
|
||||
android:id="@+id/edit_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:text="@{viewModel.isEditingMessage, default=`Hello this is John! How are you?`}"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/color_main2_400"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_header"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/cancel" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
|
|
@ -39,6 +39,13 @@
|
|||
android:visibility="@{viewModel.isReplying ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/edit_area"
|
||||
layout="@layout/chat_conversation_edit_area"
|
||||
bind:viewModel="@{viewModel}"
|
||||
android:visibility="@{viewModel.isEditing ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintTop_toBottomOf="@id/reply_area" />
|
||||
|
||||
<include
|
||||
android:id="@+id/attachments"
|
||||
layout="@layout/chat_conversation_attachments_area"
|
||||
|
|
@ -46,7 +53,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{viewModel.isFileAttachmentsListOpen ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintTop_toBottomOf="@id/reply_area" />
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_area" />
|
||||
|
||||
<androidx.emoji2.emojipicker.EmojiPickerView
|
||||
android:id="@+id/emoji_picker"
|
||||
|
|
@ -169,7 +176,7 @@
|
|||
android:visibility="@{viewModel.isCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:onClick="@{() -> viewModel.sendMessage()}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/paper_plane_right"
|
||||
android:src="@{viewModel.isEditing ? @drawable/pencil_simple : @drawable/paper_plane_right, default=@drawable/paper_plane_right}"
|
||||
android:contentDescription="@string/content_description_chat_send_message"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/message_area_background"
|
||||
|
|
|
|||
109
app/src/main/res/layout/dialog_delete_chat_message.xml
Normal file
109
app/src/main/res/layout/dialog_delete_chat_message.xml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.model.MessageDeleteDialogModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:onClick="@{() -> viewModel.dismiss()}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dialog_background"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:src="@drawable/shape_dialog_background"
|
||||
android:contentDescription="@null"
|
||||
app:layout_constraintWidth_max="@dimen/dialog_max_width"
|
||||
app:layout_constraintBottom_toBottomOf="@id/anchor"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/section_header_style"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:paddingTop="@dimen/dialog_top_bottom_margin"
|
||||
android:text="@string/conversation_dialog_delete_chat_message_title"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_constraintBottom_toTopOf="@id/cancel"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:onClick="@{() -> viewModel.deleteForEveryone()}"
|
||||
style="@style/primary_dialog_button_label_style"
|
||||
android:id="@+id/delete_for_everyone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/conversation_dialog_delete_for_everyone_label"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:enabled="@{viewModel.canBeRetracted}"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/delete_locally"
|
||||
app:layout_constraintTop_toTopOf="@id/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cancel"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:onClick="@{() -> viewModel.deleteLocally()}"
|
||||
style="@style/primary_dialog_button_label_style"
|
||||
android:id="@+id/delete_locally"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/conversation_dialog_delete_locally_label"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintStart_toEndOf="@id/delete_for_everyone"
|
||||
app:layout_constraintEnd_toStartOf="@id/cancel"
|
||||
app:layout_constraintTop_toTopOf="@id/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cancel"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:onClick="@{() -> viewModel.dismiss()}"
|
||||
style="@style/secondary_dialog_button_label_style"
|
||||
android:id="@+id/cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/dialog_cancel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintStart_toEndOf="@id/delete_locally"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_constraintBottom_toTopOf="@id/anchor"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/anchor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/dialog_top_bottom_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/cancel"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
|
|
@ -548,6 +548,13 @@
|
|||
<string name="conversation_pick_any_file_label">Choisir un fichier</string>
|
||||
<string name="conversation_file_cant_be_opened_error_toast">Impossible d\'ouvrir le fichier !</string>
|
||||
<string name="conversation_pdf_file_cant_be_opened_error_toast">Impossible d\'ouvrir un PDF protégé par mot de passe</string>
|
||||
<string name="conversation_editing_message_title">Modification du message</string>
|
||||
<string name="conversation_message_edited_label">Modifié</string>
|
||||
<string name="conversation_dialog_delete_chat_message_title">Supprimer le message ?</string>
|
||||
<string name="conversation_dialog_delete_locally_label">Pour moi</string>
|
||||
<string name="conversation_dialog_delete_for_everyone_label">Pour tout le monde</string>
|
||||
<string name="conversation_message_content_deleted_label"><i>Le message a été supprimé</i></string>
|
||||
<string name="conversation_message_content_deleted_by_us_label"><i>Vous avez supprimé le message</i></string>
|
||||
|
||||
<string name="conversation_info_participants_list_title">Participants (%s)</string>
|
||||
<string name="conversation_info_add_participants_label">Ajouter des participants</string>
|
||||
|
|
@ -805,6 +812,7 @@
|
|||
<string name="menu_invite">Inviter</string>
|
||||
<string name="menu_resend_chat_message">Ré-envoyer</string>
|
||||
<string name="menu_show_imdn">Info de réception</string>
|
||||
<string name="menu_edit_chat_message">Modifier</string>
|
||||
<string name="menu_reply_to_chat_message">Répondre</string>
|
||||
<string name="menu_forward_chat_message">Transférer</string>
|
||||
<string name="menu_copy_chat_message">Copier le texte</string>
|
||||
|
|
@ -913,6 +921,7 @@
|
|||
<string name="content_description_chat_start_voice_message_recording">Démarre l\'enregistrement d\'un message vocal</string>
|
||||
<string name="content_description_chat_send_message">Envoie le message</string>
|
||||
<string name="content_description_chat_cancel_reply">Le message ne sera plus une réponse à un précédent message</string>
|
||||
<string name="content_description_chat_cancel_edit">Annule l\'édition du message</string>
|
||||
<string name="content_description_chat_open_emoji_picker">Ouvre le selectionneur d\'emoji</string>
|
||||
<string name="content_description_chat_open_attach_file">Ouvre le selectionneur de fichier</string>
|
||||
<string name="content_description_chat_edit_conversation_subject">Cliquez pour modifier le sujet de la conversation</string>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<string name="emoji_tear" translatable="false">😢</string>
|
||||
|
||||
<string name="help_about_open_source_licenses_title" translatable="false">GNU General Public License v3.0</string>
|
||||
<string name="help_about_open_source_licenses_subtitle" translatable="false">© Belledonne Communications 2010-2024</string>
|
||||
<string name="help_about_open_source_licenses_subtitle" translatable="false">© Belledonne Communications 2010-2025</string>
|
||||
<string name="help_advanced_send_debug_logs_email_address" translatable="false">linphone-android@belledonne-communications.com</string>
|
||||
|
||||
<string name="website_contact_url" translatable="false">https://linphone.org/contact</string>
|
||||
|
|
@ -591,6 +591,13 @@
|
|||
<string name="conversation_pick_any_file_label">Pick file</string>
|
||||
<string name="conversation_file_cant_be_opened_error_toast">File can\'t be opened!</string>
|
||||
<string name="conversation_pdf_file_cant_be_opened_error_toast">Can\'t open password protected PDFs yet</string>
|
||||
<string name="conversation_editing_message_title">Message being edited</string>
|
||||
<string name="conversation_message_edited_label">Edited</string>
|
||||
<string name="conversation_dialog_delete_chat_message_title">Delete this message?</string>
|
||||
<string name="conversation_dialog_delete_locally_label">For me</string>
|
||||
<string name="conversation_dialog_delete_for_everyone_label">For everyone</string>
|
||||
<string name="conversation_message_content_deleted_label"><i>This message has been deleted</i></string>
|
||||
<string name="conversation_message_content_deleted_by_us_label"><i>You have deleted this message</i></string>
|
||||
|
||||
<string name="conversation_info_participants_list_title">Group members (%s)</string>
|
||||
<string name="conversation_info_add_participants_label">Add participants</string>
|
||||
|
|
@ -848,6 +855,7 @@
|
|||
<string name="menu_invite">Invite</string>
|
||||
<string name="menu_resend_chat_message">Re-send</string>
|
||||
<string name="menu_show_imdn">Delivery status</string>
|
||||
<string name="menu_edit_chat_message">Edit</string>
|
||||
<string name="menu_reply_to_chat_message">Reply</string>
|
||||
<string name="menu_forward_chat_message">Forward</string>
|
||||
<string name="menu_copy_chat_message">Copy</string>
|
||||
|
|
@ -956,6 +964,7 @@
|
|||
<string name="content_description_chat_start_voice_message_recording">Starts recording a voice message</string>
|
||||
<string name="content_description_chat_send_message">Sends message in conversation</string>
|
||||
<string name="content_description_chat_cancel_reply">Message will no longer be a reply to a previous message</string>
|
||||
<string name="content_description_chat_cancel_edit">Cancels message edition</string>
|
||||
<string name="content_description_chat_open_emoji_picker">Opens emoji picker</string>
|
||||
<string name="content_description_chat_open_attach_file">Opens file picker</string>
|
||||
<string name="content_description_chat_edit_conversation_subject">Click to edit the subject of this conversation</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue