Remove richtext dependency and replace with lightweight MarkdownText composable

The compose-richtext library (com.github.vitorpamplona.compose-richtext) is
replaced with a self-contained MarkdownText composable that renders the subset
of Markdown used across the app: ## headings, **bold**, [link](url), bullet
lists (- item), and numbered lists (1. item).

Affected screens: ConnectOrbotDialog, AccountBackupScreen, ExportAllAccountsScreen,
LoginScreen (two Orbot instruction blocks). RichTextDefaults is removed from Theme.

https://claude.ai/code/session_01UCZWyaGovLVMnSET3hCuKK
This commit is contained in:
Claude
2026-04-10 08:27:47 +00:00
parent b6840ac143
commit fe662a3e36
8 changed files with 167 additions and 136 deletions
-5
View File
@@ -216,11 +216,6 @@ dependencies {
implementation libs.core
implementation libs.zxing.android.embedded
// Markdown (With fix for full-image bleeds)
implementation(libs.richtext.ui)
implementation(libs.richtext.ui.material3)
implementation(libs.richtext.commonmark)
// Biometrics
implementation libs.biometric.ktx
@@ -112,14 +112,10 @@ import com.greenart7c3.nostrsigner.service.AccountExportService
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.AmberElevatedButton
import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.MarkdownText
import com.greenart7c3.nostrsigner.ui.components.MnemonicLoginInput
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.material3.RichText
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
@@ -717,31 +713,11 @@ fun SignUpPage(
)
Spacer(modifier = Modifier.height(8.dp))
val myMarkDownStyle =
RichTextDefaults.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = TextLinkStyles(
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
),
),
)
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown2)
val astNode1 =
remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
MarkdownText(
markdown = content1,
modifier = Modifier.padding(vertical = 8.dp),
style = myMarkDownStyle,
renderer = null,
) {
BasicMarkdown(astNode1)
}
)
OutlinedTextField(
value = proxyPort,
@@ -1376,31 +1352,11 @@ fun LoginPage(
)
Spacer(modifier = Modifier.height(8.dp))
val myMarkDownStyle =
RichTextDefaults.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = TextLinkStyles(
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
),
),
)
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown2)
val astNode1 =
remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
MarkdownText(
markdown = content1,
modifier = Modifier.padding(vertical = 8.dp),
style = myMarkDownStyle,
renderer = null,
) {
BasicMarkdown(astNode1)
}
)
OutlinedTextField(
value = proxyPort,
@@ -104,16 +104,11 @@ import com.greenart7c3.nostrsigner.ui.InnerQrCodeDrawer
import com.greenart7c3.nostrsigner.ui.QrCodeDrawer
import com.greenart7c3.nostrsigner.ui.components.CloseButton
import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.MarkdownText
import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
import com.greenart7c3.nostrsigner.ui.theme.fromHex
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -257,15 +252,10 @@ private fun TipsCard(expanded: Boolean, onToggle: () -> Unit) {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
val content = stringResource(R.string.account_backup_tips_md)
val astNode = remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
MarkdownText(
markdown = content,
modifier = Modifier.padding(bottom = 4.dp),
) {
BasicMarkdown(astNode)
}
)
}
}
}
@@ -300,15 +290,10 @@ private fun PasswordCard(
}
Spacer(modifier = Modifier.height(4.dp))
val tipsContent = stringResource(R.string.account_backup_tips3_md)
val tipsAstNode = remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(tipsContent)
}
RichText(
style = RichTextStyle().resolveDefaults(),
MarkdownText(
markdown = tipsContent,
modifier = Modifier.fillMaxWidth(),
) {
BasicMarkdown(tipsAstNode)
}
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
modifier = Modifier
@@ -23,7 +23,6 @@
package com.greenart7c3.nostrsigner.ui.actions
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -38,22 +37,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.TorMode
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.material3.RichText
import com.greenart7c3.nostrsigner.ui.components.MarkdownText
import kotlinx.coroutines.CancellationException
@Composable
@@ -108,33 +100,8 @@ fun ConnectOrbotScreen(
Spacer(modifier = Modifier.height(8.dp))
val myMarkDownStyle =
RichTextDefaults.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = TextLinkStyles(
SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
),
),
),
)
Row {
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
val astNode1 =
remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
style = myMarkDownStyle,
renderer = null,
) {
BasicMarkdown(astNode1)
}
}
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
MarkdownText(content1)
Spacer(modifier = Modifier.height(16.dp))
@@ -55,12 +55,7 @@ import com.greenart7c3.nostrsigner.service.AccountExportService
import com.greenart7c3.nostrsigner.service.Biometrics.authenticate
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.greenart7c3.nostrsigner.ui.components.MarkdownText
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -188,16 +183,10 @@ fun ExportAllAccountsScreen(
} else {
// Security warning
val warningContent = stringResource(R.string.export_all_accounts_warning)
val astNode = remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(warningContent)
}
RichText(
style = RichTextStyle().resolveDefaults(),
MarkdownText(
markdown = warningContent,
modifier = Modifier.padding(bottom = 16.dp),
) {
BasicMarkdown(astNode)
}
)
Text(
text = stringResource(R.string.accounts_to_export, accountCount),
@@ -0,0 +1,146 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
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.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.TextUnit
/**
* Renders a subset of Markdown as styled Compose text.
*
* Supported syntax:
* ## Heading
* **bold**
* [link text](url)
* - bullet item
* 1. numbered item
*/
@Composable
fun MarkdownText(
markdown: String,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
linkColor: Color = MaterialTheme.colorScheme.primary,
) {
val headingFontSize: TextUnit = MaterialTheme.typography.titleMedium.fontSize
val annotatedString = remember(markdown, linkColor, headingFontSize) {
buildAnnotatedString {
val lines = markdown.trim().lines()
for ((index, line) in lines.withIndex()) {
if (index > 0) append("\n")
val trimmed = line.trim()
when {
trimmed.startsWith("## ") -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontSize = headingFontSize)) {
appendMarkdownInline(trimmed.removePrefix("## "), linkColor)
}
}
trimmed.startsWith("### ") -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendMarkdownInline(trimmed.removePrefix("### "), linkColor)
}
}
trimmed.startsWith("- ") -> {
append("\u2022 ")
appendMarkdownInline(trimmed.removePrefix("- "), linkColor)
}
trimmed.matches(Regex("^\\d+\\. .+")) -> {
val dotIdx = trimmed.indexOf(". ")
append(trimmed.substring(0, dotIdx + 2))
appendMarkdownInline(trimmed.substring(dotIdx + 2), linkColor)
}
else -> {
appendMarkdownInline(trimmed, linkColor)
}
}
}
}
}
Text(
text = annotatedString,
modifier = modifier,
style = style,
)
}
private fun AnnotatedString.Builder.appendMarkdownInline(
text: String,
linkColor: Color,
) {
var remaining = text
while (remaining.isNotEmpty()) {
when {
remaining.startsWith("**") -> {
val endIdx = remaining.indexOf("**", 2)
if (endIdx != -1) {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(remaining.substring(2, endIdx))
}
remaining = remaining.substring(endIdx + 2)
} else {
append("**")
remaining = remaining.substring(2)
}
}
remaining.startsWith("[") -> {
val textEnd = remaining.indexOf("](")
if (textEnd > 0) {
val urlEnd = remaining.indexOf(")", textEnd + 2)
if (urlEnd != -1) {
val linkText = remaining.substring(1, textEnd)
val url = remaining.substring(textEnd + 2, urlEnd)
withLink(
LinkAnnotation.Url(
url,
TextLinkStyles(
SpanStyle(
color = linkColor,
textDecoration = TextDecoration.Underline,
),
),
),
) {
append(linkText)
}
remaining = remaining.substring(urlEnd + 1)
} else {
append(remaining[0])
remaining = remaining.substring(1)
}
} else {
append(remaining[0])
remaining = remaining.substring(1)
}
}
else -> {
val boldIdx = remaining.indexOf("**")
val linkIdx = remaining.indexOf("[")
val nextSpecial = when {
boldIdx == -1 && linkIdx == -1 -> remaining.length
boldIdx == -1 -> linkIdx
linkIdx == -1 -> boldIdx
else -> minOf(boldIdx, linkIdx)
}
val count = if (nextSpecial == 0) 1 else nextSpecial
append(remaining.substring(0, count))
remaining = remaining.substring(count)
}
}
}
}
@@ -20,8 +20,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import androidx.core.view.WindowCompat
import com.greenart7c3.nostrsigner.Amber
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.resolveDefaults
val Shapes =
Shapes(
@@ -39,7 +37,6 @@ val primaryColor = Color(0xFFFFCA62)
val primaryVariant = Color(0xFFC8541A)
val secondaryColor = Color(0xFFFFCA62)
val orange = Color(0xFFFF6B00)
val RichTextDefaults = RichTextStyle().resolveDefaults()
private val DarkColorPalette =
darkColorScheme(
-4
View File
@@ -13,7 +13,6 @@ material3 = "1.4.0"
nav_version = "2.9.7"
quartz = "1.08.0"
compose_ui = "1.10.6"
richtextUi = "a02a03a3e9"
roomKtx = "2.8.4"
securityCryptoKtx = "1.1.0"
zxingAndroidEmbedded = "4.3.0"
@@ -54,9 +53,6 @@ material3 = { module = "androidx.compose.material3:material3", version.ref = "ma
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "nav_version" }
quartz = { module = "com.vitorpamplona.quartz:quartz-android", version.ref = "quartz" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "collections" }
richtext-commonmark = { module = "com.github.vitorpamplona.compose-richtext:richtext-commonmark", version.ref = "richtextUi" }
richtext-ui = { module = "com.github.vitorpamplona.compose-richtext:richtext-ui", version.ref = "richtextUi" }
richtext-ui-material3 = { module = "com.github.vitorpamplona.compose-richtext:richtext-ui-material3", version.ref = "richtextUi" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" }