From aa77e7fe5aef490f22d76754288d7c1024deab3f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 1 Dec 2022 21:39:42 +0100 Subject: [PATCH] Introduce ClickableLinkText --- .../MessagesTimelineItemTextView.kt | 52 +++++++++++++- .../messages/components/html/HtmlDocument.kt | 54 ++++---------- .../components/ClickableLinkText.kt | 72 +++++++++++++++++++ 3 files changed, 135 insertions(+), 43 deletions(-) create mode 100644 libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/ClickableLinkText.kt diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt index 103deca5da..0b9a1f8211 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemTextView.kt @@ -1,10 +1,22 @@ package io.element.android.x.features.messages.components +import android.text.SpannableString +import android.text.style.URLSpan +import android.text.util.Linkify +import android.text.util.Linkify.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.core.text.util.LinkifyCompat +import io.element.android.x.designsystem.components.ClickableLinkText import io.element.android.x.features.messages.components.html.HtmlDocument import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent @@ -26,8 +38,44 @@ fun MessagesTimelineItemTextView( interactionSource = interactionSource ) } else { + val uriHandler = LocalUriHandler.current Box(modifier) { - Text(text = content.body) + val linkStyle = SpanStyle( + color = Color.Blue, + ) + val styledText = remember(content.body) { content.body.linkify(linkStyle) } + ClickableLinkText( + text = styledText, + linkAnnotationTag = "URL", + onClick = onTextClicked, + onLongClick = onTextLongClicked, + interactionSource = interactionSource + ) } } +} + +private fun String.linkify( + linkStyle: SpanStyle, +) = buildAnnotatedString { + append(this@linkify) + val spannable = SpannableString(this@linkify) + LinkifyCompat.addLinks(spannable, WEB_URLS or PHONE_NUMBERS) + + val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java) + for (span in spans) { + val start = spannable.getSpanStart(span) + val end = spannable.getSpanEnd(span) + addStyle( + start = start, + end = end, + style = linkStyle, + ) + addStringAnnotation( + tag = "URL", + annotation = span.url, + start = start, + end = end + ) + } } \ No newline at end of file diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt index fb9bd262b6..d9b9746d3f 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/components/html/HtmlDocument.kt @@ -1,24 +1,20 @@ package io.element.android.x.features.messages.components.html import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -27,6 +23,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow +import io.element.android.x.designsystem.components.ClickableLinkText import io.element.android.x.matrix.permalink.PermalinkData import io.element.android.x.matrix.permalink.PermalinkParser import org.jsoup.nodes.Document @@ -500,7 +497,7 @@ private fun AnnotatedString.Builder.appendLink(link: Element) { val permalinkData = PermalinkParser.parse(uriString) when (permalinkData) { is PermalinkData.FallbackLink -> { - pushStringAnnotation(tag = "link", annotation = link.ownText()) + pushStringAnnotation(tag = "URL", annotation = link.ownText()) withStyle( style = SpanStyle(color = Color.Blue) ) { @@ -529,41 +526,16 @@ private fun HtmlText( onLongClick: () -> Unit, interactionSource: MutableInteractionSource, ) { - val uriHandler = LocalUriHandler.current - val layoutResult = remember { mutableStateOf(null) } - val pressIndicator = Modifier.pointerInput(onClick) { - detectTapGestures( - onPress = { offset: Offset -> - val pressInteraction = PressInteraction.Press(offset) - interactionSource.emit(pressInteraction) - awaitRelease() - interactionSource.emit(PressInteraction.Release(pressInteraction)) - }, - onLongPress = { _ -> - onLongClick() - } - ) { offset -> - layoutResult.value?.let { layoutResult -> - val position = layoutResult.getOffsetForPosition(offset) - val linkAnnotations = text.getStringAnnotations("link", position, position) - if (linkAnnotations.isEmpty()) { - onClick() - } else { - uriHandler.openUri(linkAnnotations.first().item) - } - } - - } - } val inlineContentMap = emptyMap() - Text( + ClickableLinkText( text = text, - modifier = modifier.then(pressIndicator), + linkAnnotationTag = "URL", style = style, - onTextLayout = { - layoutResult.value = it - }, - inlineContent = inlineContentMap + modifier = modifier, + inlineContent = inlineContentMap, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick ) } diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/ClickableLinkText.kt new file mode 100644 index 0000000000..39edb34689 --- /dev/null +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/ClickableLinkText.kt @@ -0,0 +1,72 @@ +package io.element.android.x.designsystem.components + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle + +@Composable +fun ClickableLinkText( + text: AnnotatedString, + linkAnnotationTag: String, + onClick: () -> Unit, + onLongClick: () -> Unit, + interactionSource: MutableInteractionSource, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + inlineContent: Map = mapOf(), +) { + val uriHandler = LocalUriHandler.current + val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = Modifier.pointerInput(onClick) { + detectTapGestures( + onPress = { offset: Offset -> + val pressInteraction = PressInteraction.Press(offset) + interactionSource.emit(pressInteraction) + val isReleased = tryAwaitRelease() + if (isReleased) { + interactionSource.emit(PressInteraction.Release(pressInteraction)) + } else { + interactionSource.emit(PressInteraction.Cancel(pressInteraction)) + } + }, + onLongPress = { + onLongClick() + } + ) { offset -> + layoutResult.value?.let { layoutResult -> + val position = layoutResult.getOffsetForPosition(offset) + val linkAnnotations = + text.getStringAnnotations(linkAnnotationTag, position, position) + if (linkAnnotations.isEmpty()) { + onClick() + } else { + uriHandler.openUri(linkAnnotations.first().item) + } + } + + } + } + Text( + text = text, + modifier = modifier.then(pressIndicator), + style = style, + onTextLayout = { + layoutResult.value = it + }, + inlineContent = inlineContent + ) +} +