diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/AutoLinkText.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/AutoLinkText.kt deleted file mode 100644 index 9276bc277..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/AutoLinkText.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.geeksville.mesh.ui.common.components - -import android.text.Spannable -import android.text.Spannable.Factory -import android.text.style.URLSpan -import android.text.util.Linkify -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.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.text.util.LinkifyCompat -import com.geeksville.mesh.ui.common.theme.HyperlinkBlue - -private val DefaultTextLinkStyles = TextLinkStyles( - style = SpanStyle( - color = HyperlinkBlue, - textDecoration = TextDecoration.Underline, - ) -) - -@Composable -fun AutoLinkText( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = TextStyle.Default, - linkStyles: TextLinkStyles = DefaultTextLinkStyles, - color: Color = Color.Unspecified, -) { - val spannable = remember(text) { - linkify(text) - } - Text( - text = spannable.toAnnotatedString(linkStyles), - modifier = modifier, - style = style.copy(color = color), - ) -} - -private fun linkify(text: String) = Factory.getInstance().newSpannable(text).also { - LinkifyCompat.addLinks(it, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS) -} - -private fun Spannable.toAnnotatedString( - linkStyles: TextLinkStyles, -): AnnotatedString = buildAnnotatedString { - val spannable = this@toAnnotatedString - var lastEnd = 0 - spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> - val start = spannable.getSpanStart(span) - val end = spannable.getSpanEnd(span) - append(spannable.subSequence(lastEnd, start)) - when (span) { - is URLSpan -> withLink(LinkAnnotation.Url(url = span.url, styles = linkStyles)) { - append(spannable.subSequence(start, end)) - } - - else -> append(spannable.subSequence(start, end)) - } - lastEnd = end - } - append(spannable.subSequence(lastEnd, spannable.length)) -} - -@Preview(showBackground = true) -@Composable -private fun AutoLinkTextPreview() { - AutoLinkText("A text containing a link https://example.com") -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MDText.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MDText.kt new file mode 100644 index 000000000..ffbe2c040 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MDText.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.common.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import com.geeksville.mesh.ui.common.theme.HyperlinkBlue +import com.mikepenz.markdown.compose.components.markdownComponents +import com.mikepenz.markdown.m3.Markdown +import com.mikepenz.markdown.model.DefaultMarkdownColors +import com.mikepenz.markdown.model.DefaultMarkdownTypography + +@Composable +fun MDText( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = MaterialTheme.typography.bodyMedium, + color: Color = Color.Unspecified, +) { + val colors = + DefaultMarkdownColors( + text = color, + codeText = MaterialTheme.colorScheme.onSurface, + inlineCodeText = MaterialTheme.colorScheme.onSurface, + linkText = HyperlinkBlue, + codeBackground = MaterialTheme.colorScheme.surfaceContainerHigh, + inlineCodeBackground = MaterialTheme.colorScheme.surfaceContainerHigh, + dividerColor = MaterialTheme.colorScheme.onSurface, + tableText = MaterialTheme.colorScheme.onSurface, + tableBackground = MaterialTheme.colorScheme.surfaceContainer, + ) + + val typography = + DefaultMarkdownTypography( + // Restrict max size of the text + h1 = MaterialTheme.typography.headlineMedium.copy(color = color), + h2 = MaterialTheme.typography.headlineMedium.copy(color = color), + h3 = MaterialTheme.typography.headlineSmall.copy(color = color), + h4 = MaterialTheme.typography.titleLarge.copy(color = color), + h5 = MaterialTheme.typography.titleMedium.copy(color = color), + h6 = MaterialTheme.typography.titleSmall.copy(color = color), + text = style, + code = + MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface, + ), + inlineCode = + MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface, + background = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + quote = MaterialTheme.typography.bodyLarge.copy(color = color), + paragraph = MaterialTheme.typography.bodyMedium.copy(color = color), + ordered = MaterialTheme.typography.bodyMedium.copy(color = color), + bullet = MaterialTheme.typography.bodyMedium.copy(color = color), + list = MaterialTheme.typography.bodyMedium.copy(color = color), + link = TextStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline), + textLink = + TextLinkStyles(style = SpanStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline)), + table = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), + ) + + // Custom Markdown components to disable image rendering + val customComponents = markdownComponents(image = { /* Empty composable to disable image rendering */ }) + + Markdown( + content = text, + modifier = modifier, + colors = colors, + typography = typography, + components = customComponents, // Use custom components + ) +} + +@Preview(showBackground = true) +@Composable +private fun AutoLinkTextPreview() { + MDText( + "A text containing a link https://example.com **bold** _Italics_" + + "\n # hello \n ## hello \n ### hello \n #### hello \n ##### hello \n ###### hello \n ```code```", + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt index 73da7378f..f90bbf8fe 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt @@ -54,7 +54,7 @@ import com.geeksville.mesh.R import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.model.Message import com.geeksville.mesh.model.Node -import com.geeksville.mesh.ui.common.components.AutoLinkText +import com.geeksville.mesh.ui.common.components.MDText import com.geeksville.mesh.ui.common.components.Rssi import com.geeksville.mesh.ui.common.components.Snr import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider @@ -164,7 +164,7 @@ internal fun MessageItem( } Column(modifier = Modifier.padding(horizontal = 8.dp)) { - AutoLinkText( + MDText( modifier = Modifier.fillMaxWidth(), text = message.text, style = MaterialTheme.typography.bodyMedium,