diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt index dbf0850af..fcf2fcfb9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt @@ -20,8 +20,11 @@ package org.linphone.ui.main.chat.model import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.text.set import androidx.lifecycle.MutableLiveData import java.util.regex.Pattern import org.linphone.LinphoneApplication.Companion.coreContext @@ -34,6 +37,7 @@ import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils import org.linphone.utils.PatternClickableSpan +import org.linphone.utils.SpannableClickedListener import org.linphone.utils.TimestampUtils class ChatMessageModel @WorkerThread constructor( @@ -48,6 +52,9 @@ class ChatMessageModel @WorkerThread constructor( ) { companion object { private const val TAG = "[Chat Message Model]" + + private const val SIP_URI_REGEXP = "(?:)?" + private const val MENTION_REGEXP = "@(?:[A-Za-z0-9._-]+)" } val id = chatMessage.messageId @@ -100,14 +107,64 @@ class ChatMessageModel @WorkerThread constructor( for (content in chatMessage.contents) { if (content.isText) { val textContent = content.utf8Text.orEmpty().trim() - val spannable = Spannable.Factory.getInstance().newSpannable(textContent) + val spannableBuilder = SpannableStringBuilder(textContent) + + // Check for mentions + val chatRoom = chatMessage.chatRoom + val matcher = Pattern.compile(MENTION_REGEXP).matcher(textContent) + while (matcher.find()) { + val start = matcher.start() + val end = matcher.end() + val source = textContent.subSequence(start + 1, end) // +1 to remove @ + Log.i("$TAG Found mention [$source]") + + // Find address matching username + val address = if (chatRoom.localAddress.username == source) { + Log.i("$TAG mention found in local address") + coreContext.core.accountList.find { + it.params.identityAddress?.username == source + }?.params?.identityAddress + } else if (chatRoom.peerAddress.username == source) { + Log.i("$TAG mention found in peer address") + chatRoom.peerAddress + } else { + Log.i("$TAG looking for mention in participants") + chatRoom.participants.find { + it.address.username == source + }?.address + } + // Find display name for address + if (address != null) { + val displayName = coreContext.contactsManager.findDisplayName(address) + Log.i( + "$TAG Using display name [$displayName] instead of username [$source]" + ) + spannableBuilder.replace(start, end, "@$displayName") + val span = PatternClickableSpan.StyledClickableSpan( + object : + SpannableClickedListener { + override fun onSpanClicked(text: String) { + Log.i("$TAG Clicked on [$text] span") + } + } + ) + spannableBuilder.setSpan( + span, + start, + start + displayName.length + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + + // Add clickable span for SIP URIs text.postValue( PatternClickableSpan() .add( Pattern.compile( - "(?:)?" + SIP_URI_REGEXP ), - object : PatternClickableSpan.SpannableClickedListener { + object : SpannableClickedListener { @UiThread override fun onSpanClicked(text: String) { coreContext.postOnCoreThread { @@ -121,7 +178,8 @@ class ChatMessageModel @WorkerThread constructor( } } } - ).build(spannable) + ) + .build(spannableBuilder) ) textFound = true } diff --git a/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt b/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt index 19b1f8735..13ac79337 100644 --- a/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt +++ b/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt @@ -24,8 +24,11 @@ import android.text.Spanned import android.text.style.ClickableSpan import android.view.View import android.widget.TextView +import androidx.annotation.AnyThread +import androidx.annotation.UiThread import java.util.regex.Pattern +@AnyThread class PatternClickableSpan { private var patterns: ArrayList = ArrayList() @@ -34,18 +37,14 @@ class PatternClickableSpan { var listener: SpannableClickedListener ) - interface SpannableClickedListener { - fun onSpanClicked(text: String) - } - - inner class StyledClickableSpan(var item: SpannablePatternItem) : ClickableSpan() { + class StyledClickableSpan(var listener: SpannableClickedListener) : ClickableSpan() { override fun onClick(widget: View) { val tv = widget as TextView val span = tv.text as Spanned val start = span.getSpanStart(this) val end = span.getSpanEnd(this) val text = span.subSequence(start, end) - item.listener.onSpanClicked(text.toString()) + listener.onSpanClicked(text.toString()) } } @@ -57,17 +56,21 @@ class PatternClickableSpan { return this } - fun build(editable: CharSequence?): SpannableStringBuilder { - val ssb = SpannableStringBuilder(editable) + fun build(ssb: SpannableStringBuilder): SpannableStringBuilder { for (item in patterns) { val matcher = item.pattern.matcher(ssb) while (matcher.find()) { val start = matcher.start() val end = matcher.end() - val url = StyledClickableSpan(item) + val url = StyledClickableSpan(item.listener) ssb.setSpan(url, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } } return ssb } } + +interface SpannableClickedListener { + @UiThread + fun onSpanClicked(text: String) +} diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_reply.xml b/app/src/main/res/drawable/shape_chat_bubble_reply.xml similarity index 77% rename from app/src/main/res/drawable/shape_chat_bubble_incoming_reply.xml rename to app/src/main/res/drawable/shape_chat_bubble_reply.xml index 39659b658..98ea7684c 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_incoming_reply.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_reply.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index b6f8501df..a06f16efb 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -75,7 +75,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="20dp" - android:background="@drawable/shape_chat_bubble_incoming_reply" + android:background="@drawable/shape_chat_bubble_reply" android:visibility="@{model.isReply ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toTopOf="@id/reply" app:layout_constraintStart_toStartOf="@id/reply" diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml index c1f55c211..4fd51395a 100644 --- a/app/src/main/res/layout/chat_bubble_outgoing.xml +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -45,7 +45,7 @@ android:onClick="@{scrollToRepliedMessageClickListener}" android:layout_width="0dp" android:layout_height="0dp" - android:background="@drawable/shape_chat_bubble_incoming_reply" + android:background="@drawable/shape_chat_bubble_reply" android:visibility="@{model.isReply ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toTopOf="@id/reply" app:layout_constraintStart_toStartOf="@id/reply"