From fe662a3e36a57785453ecfebc025936adf5a3ea1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 08:27:47 +0000 Subject: [PATCH] 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 --- app/build.gradle | 5 - .../greenart7c3/nostrsigner/ui/LoginScreen.kt | 58 +------ .../ui/actions/AccountBackupScreen.kt | 29 +--- .../ui/actions/ConnectOrbotDialog.kt | 39 +---- .../ui/actions/ExportAllAccountsScreen.kt | 19 +-- .../nostrsigner/ui/components/MarkdownText.kt | 146 ++++++++++++++++++ .../greenart7c3/nostrsigner/ui/theme/Theme.kt | 3 - gradle/libs.versions.toml | 4 - 8 files changed, 167 insertions(+), 136 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MarkdownText.kt diff --git a/app/build.gradle b/app/build.gradle index 0c0dbb26..48d61895 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index 49f6e79f..d3b03d02 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -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, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt index 6609100e..6bb17949 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt index fe663657..8216ef59 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt @@ -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)) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt index 0f027f4e..57d60ba7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt @@ -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), diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MarkdownText.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MarkdownText.kt new file mode 100644 index 00000000..46b45c56 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MarkdownText.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt index d2bcbdb6..f114bda3 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt @@ -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( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 041fa74c..43dbe8f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }