From 51fe66cd884ea1e239f11d81d82da62616344e34 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 22 Sep 2026 15:29:20 +0200 Subject: [PATCH] Add linkification to the new timeline renderer (#7731) * Add linkification to the new timeline renderer Reuse `AnnotatedString.linkify` for now, even though it creates `Spannable` instances for that: it's the closest one to what we already had. Also, make sure we use the same URL annotation identifier in the renderer and the linkificator. * Update screenshots --------- Co-authored-by: ElementBot --- .../components/ClickableLinkText.kt | 8 ++++--- .../htmlrenderer/api/HtmlMessageParser.kt | 3 ++- .../htmlrenderer/api/ui/HtmlMessageContent.kt | 13 ++++++++-- .../api/ui/HtmlMessageContentPreview.kt | 24 +++++++++++++++++++ ...er.api.ui_HtmlMessageContent_Day_11_en.png | 3 +++ ....api.ui_HtmlMessageContent_Night_11_en.png | 3 +++ 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.api.ui_HtmlMessageContent_Day_11_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.api.ui_HtmlMessageContent_Night_11_en.png diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index a70aafff36..f2b2a47f16 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -32,8 +32,8 @@ import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview -import androidx.core.text.util.LinkifyCompat import io.element.android.compound.theme.LinkColor +import io.element.android.libraries.androidutils.text.LinkifyHelper import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.Text @@ -142,7 +142,7 @@ fun ClickableLinkText( fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString { val original = this val spannable = SpannableString.valueOf(this.text) - LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) + LinkifyHelper.linkify(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES) val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java) return buildAnnotatedString { @@ -150,7 +150,9 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString { for (span in spans) { val start = spannable.getSpanStart(span) val end = spannable.getSpanEnd(span) - if (original.getLinkAnnotations(start, end).isEmpty() && original.getStringAnnotations("URL", start, end).isEmpty()) { + if (original.getLinkAnnotations(start, end).isEmpty() && + original.getStringAnnotations(LINK_TAG, start, end).isEmpty() && + original.getStringAnnotations("inline_code", start, end).isEmpty()) { // Prevent linkifying domains in user or room handles (@user:domain.com, #room:domain.com) if (start > 0 && !spannable[start - 1].isWhitespace()) continue diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt index f0c3f67249..95343313db 100644 --- a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt +++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.htmlrenderer.api +import io.element.android.libraries.designsystem.components.LINK_TAG import org.jsoup.nodes.Document /** @@ -28,7 +29,7 @@ interface HtmlMessageParser { * Tag used for the string annotation attached to link (``) text ranges in a * [ParagraphNode.text]. The annotation value is the link target URL. */ - const val LINK_ANNOTATION_TAG = "url" + const val LINK_ANNOTATION_TAG = LINK_TAG /** * Tag used for the string annotation attached to inline code (``) text ranges in a diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContent.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContent.kt index 25b1f95338..ecb13310be 100644 --- a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContent.kt +++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContent.kt @@ -70,6 +70,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.linkify import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.htmlrenderer.api.BlockNode @@ -204,7 +205,9 @@ private fun ParagraphView( val codeBackgroundColor = ElementTheme.colors.bgSubtleSecondary val codeBorderColor = ElementTheme.colors.borderInteractiveSecondary val styledText = remember(node.text, linkColor) { - node.text.applyLinkStyles(linkColor = linkColor) + node.text + .linkify(SpanStyle(color = linkColor)) + .applyLinkStyles(linkColor = linkColor) } val inlineContent = rememberMentionInlineContent(node.inlineContent, context) val layoutResult = remember { mutableStateOf(null) } @@ -411,7 +414,13 @@ private fun AnnotatedString.applyLinkStyles(linkColor: Color): AnnotatedString { if (linkRanges.isEmpty()) return this return buildAnnotatedString { append(this@applyLinkStyles) - linkRanges.forEach { addStyle(SpanStyle(color = linkColor), it.start, it.end) } + linkRanges.forEach { linkRange -> + val containsColorSpan = + spanStyles.any { colorRange -> colorRange.start == linkRange.start && colorRange.end == linkRange.end && colorRange.item.color == linkColor } + if (!containsColorSpan) { + addStyle(SpanStyle(color = linkColor), linkRange.start, linkRange.end) + } + } } } diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContentPreview.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContentPreview.kt index 117764f02d..47da9af3bb 100644 --- a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContentPreview.kt +++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/ui/HtmlMessageContentPreview.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withAnnotation import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -65,6 +66,7 @@ internal class HtmlMessageContentPreviewParam : PreviewParameterProvider