Merge pull request #2040 from element-hq/feature/bma/fixLinkify

fix linkify
This commit is contained in:
Benoit Marty 2023-12-18 14:51:14 +01:00 committed by GitHub
commit d6ada81bcd
9 changed files with 127 additions and 53 deletions

View file

@ -21,6 +21,7 @@ import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.buildSpannedString
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat
import io.element.android.features.location.api.Location
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
@ -68,12 +69,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
return when (val messageType = content.type) {
is EmoteMessageType -> TimelineItemEmoteContent(
body = "* $senderDisplayName ${messageType.body}",
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName"),
isEdited = content.isEdited,
)
is EmoteMessageType -> {
val emoteBody = "* $senderDisplayName ${messageType.body}"
TimelineItemEmoteContent(
body = emoteBody,
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
isEdited = content.isEdited,
)
}
is ImageMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemImageContent(
@ -171,21 +175,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
is NoticeMessageType -> TimelineItemNoticeContent(
body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
formattedBody = parseHtml(messageType.formatted),
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
isEdited = content.isEdited,
)
is TextMessageType -> {
TimelineItemTextContent(
body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
formattedBody = parseHtml(messageType.formatted),
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
isEdited = content.isEdited,
)
}
is OtherMessageType -> TimelineItemTextContent(
body = messageType.body,
htmlDocument = null,
formattedBody = null,
formattedBody = messageType.body.withLinks(),
isEdited = content.isEdited,
)
}
@ -226,7 +230,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
Pair(start, end)
}
// Find and set as URLSpans any links present in the text
LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS)
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
for ((urlSpan, location) in oldURLSpans) {
val (start, end) = location
@ -237,3 +241,11 @@ class TimelineItemContentMessageFactory @Inject constructor(
return this
}
}
@Suppress("USELESS_ELVIS")
private fun String.withLinks(): CharSequence? {
/* Note: toSpannable() can return null when running unit tests */
val spannable = toSpannable() ?: return null
val addedLinks = LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
return spannable.takeIf { addedLinks }
}

View file

@ -17,8 +17,9 @@
package io.element.android.features.messages.impl.timeline.factories.event
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.URLSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
@ -144,9 +145,40 @@ class TimelineItemContentMessageFactoryTest {
assertThat(result).isEqualTo(expected)
}
@Test
fun `test create TextMessageType with simple link`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
senderDisplayName = "Bob",
eventId = AN_EVENT_ID,
) as TimelineItemTextContent
val expected = TimelineItemTextContent(
body = "https://www.example.org",
htmlDocument = null,
plainText = "https://www.example.org",
isEdited = false,
formattedBody = buildSpannedString {
inSpans(URLSpan("https://www.example.org")) {
append("https://www.example.org")
}
}
)
assertThat(result.body).isEqualTo(expected.body)
assertThat(result.htmlDocument).isEqualTo(expected.htmlDocument)
assertThat(result.plainText).isEqualTo(expected.plainText)
assertThat(result.isEdited).isEqualTo(expected.isEdited)
assertThat(result.formattedBody).isInstanceOf(Spanned::class.java)
val spanned = result.formattedBody as Spanned
assertThat(spanned.toString()).isEqualTo("https://www.example.org")
val urlSpans = spanned.getSpans(0, spanned.length, URLSpan::class.java)
assertThat(urlSpans).hasLength(1)
assertThat(urlSpans[0].url).isEqualTo("https://www.example.org")
}
@Test
fun `test create TextMessageType with HTML formatted body`() = runTest {
val expected = SpannableStringBuilder().apply {
val expected = buildSpannedString {
append("link to ")
inSpans(URLSpan("https://matrix.org")) {
append("https://matrix.org")
@ -160,10 +192,12 @@ class TimelineItemContentMessageFactoryTest {
htmlConverterTransform = { expected }
)
val result = sut.create(
content = createMessageContent(type = TextMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
)),
content = createMessageContent(
type = TextMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
)
),
senderDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
@ -176,10 +210,12 @@ class TimelineItemContentMessageFactoryTest {
htmlConverterTransform = { it }
)
val result = sut.create(
content = createMessageContent(type = TextMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
)),
content = createMessageContent(
type = TextMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
)
),
senderDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
@ -520,10 +556,12 @@ class TimelineItemContentMessageFactoryTest {
fun `test create NoticeMessageType with HTML formatted body`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = NoticeMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)),
content = createMessageContent(
type = NoticeMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)
),
senderDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
@ -552,10 +590,12 @@ class TimelineItemContentMessageFactoryTest {
fun `test create EmoteMessageType with HTML formatted body`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = EmoteMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)),
content = createMessageContent(
type = EmoteMessageType(
body = "body",
formatted = FormattedBody(MessageFormat.HTML, "formatted")
)
),
senderDisplayName = "Bob",
eventId = AN_EVENT_ID,
)