diff --git a/CHANGELOG.md b/CHANGELOG.md index 78544e9c8..754f2837f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Group changes to describe their impact on the project, as follows: - Support right click on some items to open bottom sheet/menu - Added toggle speaker action in active call notification - Increased text size for chat messages that only contains emoji(s) +- Use user-input to filter participants list after typing "@" in conversation send area - Handle read-only CardDAV address books, disable edit/delete menus for contacts in read-only FriendList - Added swipe/pull to refresh on contacts list of a CardDAV addressbook has been configured to force the synchronization - Show information to user when filtering contacts doesn't show them all and user may have to refine it's search @@ -52,6 +53,9 @@ Group changes to describe their impact on the project, as follows: - Added more info into StartupListener logs - Updated password forgotten procedure, will use online account manager platform +### Fixed +- Copy raw message content instead of modified one when it contains a participant mention ("@username") + ## [6.0.21] - 2025-12-16 ### Added 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 a79f34a11..aa18972e6 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 @@ -283,13 +283,22 @@ open class ConversationFragment : SlidingPaneChildFragment() { override fun afterTextChanged(editable: Editable?) { if (viewModel.isGroup.value == true) { - sendMessageViewModel.closeParticipantsList() - val split = editable.toString().split(" ") - for (part in split) { - if (part == "@") { - Log.i("$TAG '@' found, opening participants list") - sendMessageViewModel.openParticipantsList() + if (split.isNotEmpty()) { + val lastPart = split.last() + if (lastPart.isNotEmpty() && lastPart.startsWith("@")) { + coreContext.postOnCoreThread { + val filter = if (lastPart.length > 1) lastPart.substring(1) else "" + sendMessageViewModel.filterParticipantsList(filter) + } + + if (sendMessageViewModel.isParticipantsListOpen.value == false) { + Log.i("$TAG '@' found, opening participants list") + sendMessageViewModel.openParticipantsList() + } + } else if (sendMessageViewModel.isParticipantsListOpen.value == true) { + Log.i("$TAG Closing participants list") + sendMessageViewModel.closeParticipantsList() } } } 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 99cad6b9b..310350dfa 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 @@ -25,6 +25,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan +import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MediatorLiveData @@ -151,6 +152,8 @@ class MessageModel val isSelected = MutableLiveData() + private var rawTextContent: String = "" + // Below are for conferences info val meetingFound = MutableLiveData() @@ -436,6 +439,11 @@ class MessageModel avatarModel.postValue(avatar) } + @AnyThread + fun getRawTextContent(): String { + return rawTextContent + } + @WorkerThread private fun computeContentsList() { Log.d("$TAG Computing message contents list") @@ -686,10 +694,10 @@ class MessageModel @WorkerThread private fun computeTextContent(content: Content, highlight: String) { - val textContent = content.utf8Text.orEmpty().trim() - val spannableBuilder = SpannableStringBuilder(textContent) + rawTextContent = content.utf8Text.orEmpty().trim() + val spannableBuilder = SpannableStringBuilder(rawTextContent) - val emojiOnly = AppUtils.isTextOnlyContainsEmoji(textContent) + val emojiOnly = AppUtils.isTextOnlyContainsEmoji(rawTextContent) isTextEmoji.postValue(emojiOnly) if (emojiOnly) { text.postValue(spannableBuilder) @@ -698,7 +706,7 @@ class MessageModel // Check for search if (highlight.isNotEmpty()) { - val indexStart = textContent.indexOf(highlight, 0, ignoreCase = true) + val indexStart = rawTextContent.indexOf(highlight, 0, ignoreCase = true) if (indexStart >= 0) { isTextHighlighted = true val indexEnd = indexStart + highlight.length @@ -713,12 +721,12 @@ class MessageModel // Check for mentions val chatRoom = chatMessage.chatRoom - val matcher = Pattern.compile(MENTION_REGEXP).matcher(textContent) + val matcher = Pattern.compile(MENTION_REGEXP).matcher(rawTextContent) var offset = 0 while (matcher.find()) { val start = matcher.start() val end = matcher.end() - val source = textContent.subSequence(start + 1, end) // +1 to remove @ + val source = rawTextContent.subSequence(start + 1, end) // +1 to remove @ Log.d("$TAG Found mention [$source]") // Find address matching username 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 f6957b3eb..56af87113 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 @@ -150,7 +150,7 @@ class ChatMessageLongPressViewModel : GenericViewModel() { fun copyClickListener() { Log.i("$TAG Copying message text into clipboard") - val text = messageModel.value?.text?.value?.toString() + val text = messageModel.value?.getRawTextContent() val clipboard = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val label = "Message" clipboard.setPrimaryClip(ClipData.newPlainText(label, text)) 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 4b72bf66f..283b13fd2 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 @@ -110,6 +110,8 @@ class SendMessageInConversationViewModel val voiceRecordPlayerPosition = MutableLiveData() + val isComputingParticipantsList = MutableLiveData() + private lateinit var voiceRecordPlayer: Player private val playerListener = PlayerListener { @@ -143,6 +145,8 @@ class SendMessageInConversationViewModel private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null + private var participantsListFilter = "" + private val chatRoomListener = object : ChatRoomListenerStub() { @WorkerThread override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { @@ -163,6 +167,7 @@ class SendMessageInConversationViewModel isKeyboardOpen.value = false isEmojiPickerOpen.value = false areFilePickersOpen.value = false + isParticipantsListOpen.value = false isVoiceRecording.value = false isPlayingVoiceRecord.value = false isCallConversation.value = false @@ -409,6 +414,10 @@ class SendMessageInConversationViewModel @UiThread fun closeParticipantsList() { isParticipantsListOpen.value = false + coreContext.postOnCoreThread { + participantsListFilter = "" + computeParticipantsList() + } } @UiThread @@ -571,7 +580,32 @@ class SendMessageInConversationViewModel } @WorkerThread - private fun computeParticipantsList() { + fun filterParticipantsList(filter: String) { + Log.i("$TAG Filtering participants list using user-input [$filter]") + if (filter.isEmpty() && participantsListFilter.isNotEmpty()) { + participantsListFilter = "" + computeParticipantsList() + return + } + + if (filter.length >= participantsListFilter.length) { + isComputingParticipantsList.postValue(true) + participantsListFilter = filter + val currentList = participants.value.orEmpty() + val newList = currentList.filter { + it.address.asStringUriOnly().contains(filter) || it.avatarModel.contactName?.contains(filter) == true + } + participants.postValue(newList as ArrayList) + isComputingParticipantsList.postValue(false) + } else { + participantsListFilter = filter + computeParticipantsList(filter) + } + } + + @WorkerThread + private fun computeParticipantsList(filter: String = "") { + isComputingParticipantsList.postValue(true) val participantsList = arrayListOf() for (participant in chatRoom.participants) { @@ -580,14 +614,18 @@ class SendMessageInConversationViewModel coreContext.postOnCoreThread { val username = clicked.address.username if (!username.isNullOrEmpty()) { - participantUsernameToAddEvent.postValue(Event(username)) + participantUsernameToAddEvent.postValue(Event(username.substring(participantsListFilter.length))) } } }) - participantsList.add(model) + + if (filter.isEmpty() || participant.address.asStringUriOnly().contains(filter) || model.avatarModel.contactName?.contains(filter) == true) { + participantsList.add(model) + } } participants.postValue(participantsList) + isComputingParticipantsList.postValue(false) } @WorkerThread diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index 824ea77f0..f09e15593 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -22,6 +22,18 @@ android:importantForAccessibility="no" app:layout_constraintTop_toTopOf="parent"/> + + + app:layout_constraintTop_toBottomOf="@id/participants_header"> + + Pour tout le monde Le message a été supprimé Vous avez supprimé le message + Participants Participants (%s) Ajouter des participants diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5fb0c1668..dcf1d2cd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -609,6 +609,7 @@ For everyone This message has been deleted You have deleted this message + Participants Group members (%s) Add participants