mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 00:48:40 +00:00
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 <android@element.io>
This commit is contained in:
committed by
GitHub
parent
9b3d4c7c27
commit
51fe66cd88
+5
-3
@@ -32,8 +32,8 @@ import androidx.compose.ui.text.TextLayoutResult
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.core.text.util.LinkifyCompat
|
|
||||||
import io.element.android.compound.theme.LinkColor
|
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.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
@@ -142,7 +142,7 @@ fun ClickableLinkText(
|
|||||||
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
||||||
val original = this
|
val original = this
|
||||||
val spannable = SpannableString.valueOf(this.text)
|
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)
|
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
|
||||||
return buildAnnotatedString {
|
return buildAnnotatedString {
|
||||||
@@ -150,7 +150,9 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
|||||||
for (span in spans) {
|
for (span in spans) {
|
||||||
val start = spannable.getSpanStart(span)
|
val start = spannable.getSpanStart(span)
|
||||||
val end = spannable.getSpanEnd(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)
|
// Prevent linkifying domains in user or room handles (@user:domain.com, #room:domain.com)
|
||||||
if (start > 0 && !spannable[start - 1].isWhitespace()) continue
|
if (start > 0 && !spannable[start - 1].isWhitespace()) continue
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.htmlrenderer.api
|
package io.element.android.libraries.htmlrenderer.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.designsystem.components.LINK_TAG
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ interface HtmlMessageParser {
|
|||||||
* Tag used for the string annotation attached to link (`<a href>`) text ranges in a
|
* Tag used for the string annotation attached to link (`<a href>`) text ranges in a
|
||||||
* [ParagraphNode.text]. The annotation value is the link target URL.
|
* [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 (`<code>`) text ranges in a
|
* Tag used for the string annotation attached to inline code (`<code>`) text ranges in a
|
||||||
|
|||||||
+11
-2
@@ -70,6 +70,7 @@ import androidx.compose.ui.unit.Density
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.text.toPx
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.htmlrenderer.api.BlockNode
|
import io.element.android.libraries.htmlrenderer.api.BlockNode
|
||||||
@@ -204,7 +205,9 @@ private fun ParagraphView(
|
|||||||
val codeBackgroundColor = ElementTheme.colors.bgSubtleSecondary
|
val codeBackgroundColor = ElementTheme.colors.bgSubtleSecondary
|
||||||
val codeBorderColor = ElementTheme.colors.borderInteractiveSecondary
|
val codeBorderColor = ElementTheme.colors.borderInteractiveSecondary
|
||||||
val styledText = remember(node.text, linkColor) {
|
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 inlineContent = rememberMentionInlineContent(node.inlineContent, context)
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
@@ -411,7 +414,13 @@ private fun AnnotatedString.applyLinkStyles(linkColor: Color): AnnotatedString {
|
|||||||
if (linkRanges.isEmpty()) return this
|
if (linkRanges.isEmpty()) return this
|
||||||
return buildAnnotatedString {
|
return buildAnnotatedString {
|
||||||
append(this@applyLinkStyles)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withAnnotation
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
@@ -65,6 +66,7 @@ internal class HtmlMessageContentPreviewParam : PreviewParameterProvider<Documen
|
|||||||
mentionOwn(),
|
mentionOwn(),
|
||||||
nestedList(),
|
nestedList(),
|
||||||
nestedListWithComplexContents(),
|
nestedListWithComplexContents(),
|
||||||
|
linkifiedParagraph(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun richParagraph() = document(
|
private fun richParagraph() = document(
|
||||||
@@ -82,6 +84,28 @@ internal class HtmlMessageContentPreviewParam : PreviewParameterProvider<Documen
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun linkifiedParagraph() = document(
|
||||||
|
paragraph(
|
||||||
|
buildAnnotatedString {
|
||||||
|
append("Hello me@matrix.org, check out element.io and and call +1 234 567 8900, then get in touch with ")
|
||||||
|
appendInlineContent(MENTION_ID, "@alice:example.org")
|
||||||
|
append(".\n\n")
|
||||||
|
append("Also, ignore this link: ")
|
||||||
|
val start = length
|
||||||
|
append("https://matrix.org")
|
||||||
|
addStringAnnotation(LINK_ANNOTATION_TAG, "https://matrix.org", start, length)
|
||||||
|
append(" and this one: ")
|
||||||
|
withAnnotation(INLINE_CODE_ANNOTATION_TAG, "") {
|
||||||
|
append("https://element.io")
|
||||||
|
}
|
||||||
|
append(".")
|
||||||
|
},
|
||||||
|
inlineContent = persistentMapOf(
|
||||||
|
MENTION_ID to MentionNodeContent.User(displayText = "@alice", userId = UserId("@alice:example.org")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private fun inlineCode() = document(
|
private fun inlineCode() = document(
|
||||||
paragraph(
|
paragraph(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user