From 6cd4f974da5f17a7ce9713806154244075aceccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 20 May 2024 08:45:31 +0200 Subject: [PATCH 1/4] When linkifying HTML messages, give priority to explicit link tags --- .../event/TimelineItemContentMessageFactory.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 2aba9c36699..5c0623b4808 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -268,12 +268,16 @@ class TimelineItemContentMessageFactory @Inject constructor( } // Find and set as URLSpans any links present in the text LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) - // Restore old spans if they don't conflict with the new ones + // Restore old spans, remove new ones if there is a conflict for ((urlSpan, location) in oldURLSpans) { val (start, end) = location - if (getSpans(start, end).isEmpty()) { - setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val addedSpans = getSpans(start, end).orEmpty() + if (addedSpans.isNotEmpty()) { + for (span in addedSpans) { + removeSpan(span) + } } + setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } return this } From dedad1383fae40b624153dd34d5f208e9e6ab757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 20 May 2024 09:20:48 +0200 Subject: [PATCH 2/4] Add test --- .../TimelineItemContentMessageFactoryTest.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index 35de78f65b3..0cfd857a5f8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -16,7 +16,9 @@ package io.element.android.features.messages.impl.timeline.factories.event +import android.net.Uri import android.text.SpannableString +import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.URLSpan import androidx.core.text.buildSpannedString @@ -46,6 +48,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -641,6 +644,31 @@ class TimelineItemContentMessageFactoryTest { assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted")) } + @Test + fun `a message with existing URLSpans keeps it after linkification`() = runTest { + val expectedSpanned = SpannableStringBuilder().apply { + append("Test ") + inSpans(URLSpan("https://www.example.org")) { + append("me@matrix.org") + } + } + val sut = createTimelineItemContentMessageFactory( + htmlConverterTransform = { expectedSpanned }, + permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) } + ) + val result = sut.create( + content = createMessageContent( + type = TextMessageType( + body = "Test [me@matrix.org](https://www.example.org)", + formatted = FormattedBody(MessageFormat.HTML, "Test me@matrix.org") + ) + ), + senderDisambiguatedDisplayName = "Bob", + eventId = AN_EVENT_ID, + ) + assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expectedSpanned) + } + private fun createMessageContent( body: String = "Body", inReplyTo: InReplyTo? = null, @@ -660,12 +688,13 @@ class TimelineItemContentMessageFactoryTest { private fun createTimelineItemContentMessageFactory( featureFlagService: FeatureFlagService = FakeFeatureFlagService(), htmlConverterTransform: (String) -> CharSequence = { it }, + permalinkParser: FakePermalinkParser = FakePermalinkParser(), ) = TimelineItemContentMessageFactory( fileSizeFormatter = FakeFileSizeFormatter(), fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), featureFlagService = featureFlagService, htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform), - permalinkParser = FakePermalinkParser(), + permalinkParser = permalinkParser, ) private fun createStickerContent( From a09d223b88e7beed52b25eee7795c07240f5d081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 20 May 2024 09:31:19 +0200 Subject: [PATCH 3/4] Add changelog --- changelog.d/2291.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2291.bugfix diff --git a/changelog.d/2291.bugfix b/changelog.d/2291.bugfix new file mode 100644 index 00000000000..73e84559a79 --- /dev/null +++ b/changelog.d/2291.bugfix @@ -0,0 +1 @@ +Make sure explicit links in messages take priority over links found by linkification (urls, emails, phone numbers, etc.) From f3babf8fade851627e27c027437d66f287e6c6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 20 May 2024 12:29:55 +0200 Subject: [PATCH 4/4] Suppress warning in tests --- .../factories/event/TimelineItemContentMessageFactoryTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index 0cfd857a5f8..6d8fb1ad9a7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -78,6 +78,7 @@ import org.robolectric.RobolectricTestRunner import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes +@Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) class TimelineItemContentMessageFactoryTest { @Test