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,