Started mention of participant in chat message

This commit is contained in:
Sylvain Berfini 2023-10-31 12:34:57 +01:00
parent 5e069033b3
commit 7aed1d83e3
5 changed files with 77 additions and 16 deletions

View file

@ -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 = "(?:<?sips?:)[a-zA-Z0-9+_.\\-]+(?:@([a-zA-Z0-9+_.\\-;=~]+))+(>)?"
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(
"(?:<?sips?:)[a-zA-Z0-9+_.\\-]+(?:@([a-zA-Z0-9+_.\\-;=~]+))+(>)?"
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
}

View file

@ -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<SpannablePatternItem> = 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)
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="16dp" android:topLeftRadius="16dp" />
<solid android:color="@color/gray_main2_100"/>
<solid android:color="@color/gray_200"/>
</shape>

View file

@ -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"

View file

@ -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"