@ -16,6 +16,9 @@
@@ -16,6 +16,9 @@
package io.element.android.libraries.designsystem.components
import android.text.SpannableString
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
@ -32,27 +35,64 @@ import androidx.compose.ui.platform.LocalUriHandler
@@ -32,27 +35,64 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
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.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.LinkColor
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@Composable
fun ClickableLinkText (
text : String ,
interactionSource : MutableInteractionSource ,
modifier : Modifier = Modifier ,
linkify : Boolean = true ,
linkAnnotationTag : String = " " ,
onClick : ( ) -> Unit = { } ,
onLongClick : ( ) -> Unit = { } ,
style : TextStyle = LocalTextStyle . current ,
inlineContent : ImmutableMap < String , InlineTextContent > = persistentMapOf ( ) ,
) {
ClickableLinkText (
annotatedString = AnnotatedString ( text ) ,
interactionSource = interactionSource ,
modifier = modifier ,
linkify = linkify ,
linkAnnotationTag = linkAnnotationTag ,
onClick = onClick ,
onLongClick = onLongClick ,
style = style ,
inlineContent = inlineContent ,
)
}
@OptIn ( ExperimentalTextApi :: class )
@Composable
fun ClickableLinkText (
text : AnnotatedString ,
annotatedString : AnnotatedString ,
interactionSource : MutableInteractionSource ,
modifier : Modifier = Modifier ,
linkify : Boolean = true ,
linkAnnotationTag : String = " " ,
onClick : ( ) -> Unit = { } ,
onLongClick : ( ) -> Unit = { } ,
style : TextStyle = LocalTextStyle . current ,
inlineContent : ImmutableMap < String , InlineTextContent > = persistentMapOf ( ) ,
) {
val processedText = remember ( annotatedString ) {
if ( linkify ) {
annotatedString . linkify ( SpanStyle ( color = LinkColor ) )
} else {
annotatedString
}
}
val uriHandler = LocalUriHandler . current
val layoutResult = remember { mutableStateOf < TextLayoutResult ? > ( null ) }
val pressIndicator = Modifier . pointerInput ( onClick ) {
@ -73,10 +113,10 @@ fun ClickableLinkText(
@@ -73,10 +113,10 @@ fun ClickableLinkText(
) { offset ->
layoutResult . value ?. let { layoutResult ->
val position = layoutResult . getOffsetForPosition ( offset )
val linkUrlAnnotations = text . getUrlAnnotations ( position , position )
val linkUrlAnnotations = annotatedString . getUrlAnnotations ( position , position )
. map { AnnotatedString . Range ( it . item . url , it . start , it . end , linkAnnotationTag ) }
val linkStringAnnotations = linkUrlAnnotations +
text . getStringAnnotations ( linkAnnotationTag , position , position )
annotatedString . getStringAnnotations ( linkAnnotationTag , position , position )
if ( linkStringAnnotations . isEmpty ( ) ) {
onClick ( )
} else {
@ -86,7 +126,7 @@ fun ClickableLinkText(
@@ -86,7 +126,7 @@ fun ClickableLinkText(
}
}
Text (
text = t ext,
text = processedT ext,
modifier = modifier . then ( pressIndicator ) ,
style = style ,
onTextLayout = {
@ -97,6 +137,37 @@ fun ClickableLinkText(
@@ -97,6 +137,37 @@ fun ClickableLinkText(
)
}
@OptIn ( ExperimentalTextApi :: class )
fun AnnotatedString . linkify ( linkStyle : SpanStyle ) : AnnotatedString {
val original = this
val spannable = SpannableString ( this . text )
LinkifyCompat . addLinks ( spannable , Linkify . WEB _URLS or Linkify . PHONE _NUMBERS )
val spans = spannable . getSpans ( 0 , spannable . length , URLSpan :: class . java )
return buildAnnotatedString {
append ( original )
for ( span in spans ) {
val start = spannable . getSpanStart ( span )
val end = spannable . getSpanEnd ( span )
if ( original . getUrlAnnotations ( start , end ) . isEmpty ( ) && original . getStringAnnotations ( " URL " , 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
addStyle (
start = start ,
end = end ,
style = linkStyle ,
)
addStringAnnotation (
tag = " URL " ,
annotation = span . url ,
start = start ,
end = end
)
}
}
}
}
@Preview ( group = PreviewGroup . Text )
@Composable
internal fun ClickableLinkTextPreview ( ) =
@ -105,7 +176,7 @@ internal fun ClickableLinkTextPreview() =
@@ -105,7 +176,7 @@ internal fun ClickableLinkTextPreview() =
@Composable
private fun ContentToPreview ( ) {
ClickableLinkText (
text = AnnotatedString ( " Hello " , ParagraphStyle ( ) ) ,
annotatedString = AnnotatedString ( " Hello " , ParagraphStyle ( ) ) ,
linkAnnotationTag = " " ,
onClick = { } ,
onLongClick = { } ,