Introduce ClickableLinkText

This commit is contained in:
ganfra 2022-12-01 21:39:42 +01:00
parent 275005f598
commit f76422ad48
3 changed files with 135 additions and 43 deletions

View file

@ -1,10 +1,22 @@
package io.element.android.x.features.messages.components 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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.components.html.HtmlDocument
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
@ -26,8 +38,44 @@ fun MessagesTimelineItemTextView(
interactionSource = interactionSource interactionSource = interactionSource
) )
} else { } else {
val uriHandler = LocalUriHandler.current
Box(modifier) { 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
)
}
} }

View file

@ -1,24 +1,20 @@
package io.element.android.x.features.messages.components.html package io.element.android.x.features.messages.components.html
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.* 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.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color 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.*
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow 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.PermalinkData
import io.element.android.x.matrix.permalink.PermalinkParser import io.element.android.x.matrix.permalink.PermalinkParser
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -500,7 +497,7 @@ private fun AnnotatedString.Builder.appendLink(link: Element) {
val permalinkData = PermalinkParser.parse(uriString) val permalinkData = PermalinkParser.parse(uriString)
when (permalinkData) { when (permalinkData) {
is PermalinkData.FallbackLink -> { is PermalinkData.FallbackLink -> {
pushStringAnnotation(tag = "link", annotation = link.ownText()) pushStringAnnotation(tag = "URL", annotation = link.ownText())
withStyle( withStyle(
style = SpanStyle(color = Color.Blue) style = SpanStyle(color = Color.Blue)
) { ) {
@ -529,41 +526,16 @@ private fun HtmlText(
onLongClick: () -> Unit, onLongClick: () -> Unit,
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
) { ) {
val uriHandler = LocalUriHandler.current
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(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<String, InlineTextContent>() val inlineContentMap = emptyMap<String, InlineTextContent>()
Text( ClickableLinkText(
text = text, text = text,
modifier = modifier.then(pressIndicator), linkAnnotationTag = "URL",
style = style, style = style,
onTextLayout = { modifier = modifier,
layoutResult.value = it inlineContent = inlineContentMap,
}, interactionSource = interactionSource,
inlineContent = inlineContentMap onClick = onClick,
onLongClick = onLongClick
) )
} }

View file

@ -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<String, InlineTextContent> = mapOf(),
) {
val uriHandler = LocalUriHandler.current
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(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
)
}