diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt index f74d3b5f30..9faf42d3fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/TextPillificationHelper.kt @@ -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(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(start, end).isNotEmpty()) return false diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt index 6390c2ba48..2590c9fc5c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt @@ -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(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"