Merge pull request #7354 from hayaksi1/fix/5319-linkify-matrix-uri

Turn a matrix: URI in a message into a pill
This commit is contained in:
Benoit Marty
2026-08-13 09:17:32 +02:00
committed by GitHub
2 changed files with 90 additions and 0 deletions
@@ -28,6 +28,8 @@ import io.element.android.libraries.textcomposer.mentions.getMentionSpans
import io.element.android.wysiwyg.view.spans.CodeBlockSpan
import io.element.android.wysiwyg.view.spans.InlineCodeSpan
private val MATRIX_URI_REGEX = Regex("""matrix:(?:u|user|r|room|roomid|e|event)/\S*[^\s.,;:!?)]""", RegexOption.IGNORE_CASE)
interface TextPillificationHelper {
fun pillify(text: CharSequence, pillifyPermalinks: Boolean = true): CharSequence
}
@@ -87,6 +89,7 @@ class DefaultTextPillificationHelper(
}
private fun pillifyPermalinks(text: SpannableStringBuilder) {
pillifyMatrixUris(text)
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
val start = match.range.first
val end = match.range.last + 1
@@ -99,6 +102,21 @@ class DefaultTextPillificationHelper(
}
}
private fun pillifyMatrixUris(text: SpannableStringBuilder) {
for (match in MATRIX_URI_REGEX.findAll(text)) {
val start = match.range.first
val end = match.range.last + 1
if (!text.canPillify(start, end)) continue
val mentionSpan = mentionSpanProvider.getMentionSpanFor(match.value, match.value)
if (mentionSpan != null) {
text.setSpan(mentionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
if (text.getSpans<URLSpan>(start, end).isEmpty()) {
text.setSpan(URLSpan(match.value), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
}
private fun Spanned.canPillify(start: Int, end: Int): Boolean {
if (getMentionSpans(start, end).isNotEmpty()) return false
if (getSpans<CodeBlockSpan>(start, end).isNotEmpty()) return false
@@ -9,6 +9,9 @@
package io.element.android.features.messages.impl.utils
import android.net.Uri
import android.text.Spanned
import android.text.style.URLSpan
import androidx.core.text.getSpans
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
@@ -30,6 +33,75 @@ import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class DefaultTextPillificationHelperTest : RobolectricTest() {
@Test
fun `pillify - adds pills for matrix uri user links`() {
val text = "Hello matrix:u/someone:example.com"
val userId = UserId("@someone:example.com")
val helper = aTextPillificationHelper(
permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(userId) }),
permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = {
Result.success("https://matrix.to/#/@someone:example.com")
}),
)
val pillified = helper.pillify(text)
val mentionSpans = pillified.getMentionSpans()
assertThat(mentionSpans).hasSize(1)
assertThat(mentionSpans.first().type).isInstanceOf(MentionType.User::class.java)
val spanned = pillified as Spanned
assertThat(spanned.getSpanStart(mentionSpans.first())).isEqualTo(text.indexOf("matrix:"))
assertThat(spanned.getSpanEnd(mentionSpans.first())).isEqualTo(text.length)
val urlSpans = spanned.getSpans<URLSpan>(0, spanned.length)
assertThat(urlSpans).hasLength(1)
assertThat(urlSpans.first().url).isEqualTo("matrix:u/someone:example.com")
}
@Test
fun `pillify - adds pills for matrix uri room links`() {
val text = "Join matrix:r/room:example.com now"
val roomAlias = RoomAlias("#room:example.com")
val helper = aTextPillificationHelper(
permalinkParser = FakePermalinkParser(result = {
PermalinkData.RoomLink(roomIdOrAlias = roomAlias.toRoomIdOrAlias())
}),
permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = {
Result.success("https://matrix.to/#/#room:example.com")
}),
)
val mentionSpans = helper.pillify(text).getMentionSpans()
assertThat(mentionSpans).hasSize(1)
assertThat(mentionSpans.first().type).isInstanceOf(MentionType.Room::class.java)
}
@Test
fun `pillify - a matrix uri does not swallow trailing punctuation`() {
val text = "See matrix:u/someone:example.com."
val userId = UserId("@someone:example.com")
val helper = aTextPillificationHelper(
permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(userId) }),
permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = {
Result.success("https://matrix.to/#/@someone:example.com")
}),
)
val pillified = helper.pillify(text)
val mentionSpans = pillified.getMentionSpans()
assertThat(mentionSpans).hasSize(1)
assertThat((pillified as Spanned).getSpanEnd(mentionSpans.first())).isEqualTo(text.length - 1)
}
@Test
fun `pillify - does not pillify matrix uris when permalinks are disabled`() {
val text = "Hello matrix:u/someone:example.com"
val userId = UserId("@someone:example.com")
val helper = aTextPillificationHelper(
permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(userId) }),
permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = {
Result.success("https://matrix.to/#/@someone:example.com")
}),
)
val pillified = helper.pillify(text, pillifyPermalinks = false)
assertThat(pillified.getMentionSpans()).isEmpty()
}
@Test
fun `pillify - adds pills for user ids`() {
val text = "A @user:server.com"