Introduce ClickableLinkText
This commit is contained in:
parent
275005f598
commit
f76422ad48
3 changed files with 135 additions and 43 deletions
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue