mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Turn a matrix: URI in a message into a pill
A permalink written as a matrix: URI stayed plain text, while the same link in matrix.to form became a pill. The pillifier only scanned Patterns.WEB_URL, which cannot match a URI with no web scheme, even though the permalink parser already understands matrix: URIs. Those URIs are now scanned for as well, before the web pass, since Patterns.WEB_URL otherwise claims the bare domain inside the URI and whichever pass runs first wins the range. Each match also gets a URLSpan: the timeline only reports URLSpans as clicks, so without one the pill would look right and do nothing. The composer path is untouched, since it pillifies without permalinks. Relates to https://github.com/element-hq/element-x-android/issues/5319
This commit is contained in:
+31
@@ -28,6 +28,11 @@ import io.element.android.libraries.textcomposer.mentions.getMentionSpans
|
||||
import io.element.android.wysiwyg.view.spans.CodeBlockSpan
|
||||
import io.element.android.wysiwyg.view.spans.InlineCodeSpan
|
||||
|
||||
/**
|
||||
* Matches a `matrix:` URI as defined by MSC2312, stopping before any trailing sentence punctuation.
|
||||
*/
|
||||
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 +92,9 @@ class DefaultTextPillificationHelper(
|
||||
}
|
||||
|
||||
private fun pillifyPermalinks(text: SpannableStringBuilder) {
|
||||
// Before the web pass: Patterns.WEB_URL matches the bare domain inside a matrix: URI, and
|
||||
// whichever pass runs first wins the range.
|
||||
pillifyMatrixUris(text)
|
||||
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
|
||||
val start = match.range.first
|
||||
val end = match.range.last + 1
|
||||
@@ -99,6 +107,29 @@ class DefaultTextPillificationHelper(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `Patterns.WEB_URL` cannot match a `matrix:` URI, so those permalinks were left as plain text
|
||||
* even though the parser understands them. See
|
||||
* https://github.com/element-hq/element-x-android/issues/5319
|
||||
*/
|
||||
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)
|
||||
// A `matrix:` URI gets no URLSpan from linkification, and the timeline only reports
|
||||
// URLSpans as clicks, so the pill would otherwise be inert. Adding it here also makes
|
||||
// LinkifyHelper drop the bogus web link it would otherwise find inside the URI.
|
||||
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
|
||||
|
||||
+73
@@ -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,76 @@ 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)
|
||||
// Without a URLSpan the pill would not be clickable in the timeline.
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user