refactor(RichTextViewer): move isMarkdown onto RichTextViewerState

Cleaner conceptual model: the parsed state object is the place where
all derived facts about the content live, so `isMarkdown` becomes a
field on `RichTextViewerState` (populated by `RichTextParser.parseText`
using the same cheap heuristic).

`RichTextViewer` now calls `CachedRichTextParser.parseText` once at the
top and dispatches on `state.isMarkdown` instead of running a separate
scan. `CachedRichTextParser.isMarkdown(content)` and its dedicated
`isMarkdownCache` go away — the single `richTextCache` carries the
decision alongside the parsed segments.

Inner callers (`DisplaySecretEmoji`, `MultiSetCompose` reaction
preview, `DisplayUncitedHashtags`) are unaffected: they continue to
receive a fully-parsed state with all segments populated.
This commit is contained in:
Claude
2026-05-11 00:25:36 +00:00
parent 680e5bc0be
commit 7e2e3304e1
4 changed files with 19 additions and 21 deletions
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.commons.richtext.UrlParser
object CachedRichTextParser { object CachedRichTextParser {
private val richTextCache = LruCache<Int, RichTextViewerState>(50) private val richTextCache = LruCache<Int, RichTextViewerState>(50)
private val isMarkdownCache = LruCache<Int, Boolean>(200)
private fun hashCodeCache( private fun hashCodeCache(
content: String, content: String,
@@ -70,25 +69,6 @@ object CachedRichTextParser {
newUrls newUrls
} }
} }
// Shared across every RichTextViewer instance so that the same content quoted in multiple
// notes only pays for the scan once. The decision is purely a function of `content`.
fun isMarkdown(content: String): Boolean {
val key = content.hashCode()
isMarkdownCache[key]?.let { return it }
val result = computeIsMarkdown(content)
isMarkdownCache.put(key, result)
return result
}
private fun computeIsMarkdown(content: String): Boolean =
content.startsWith("> ") ||
content.startsWith("# ") ||
content.contains("##") ||
content.contains("__") ||
content.contains("**") ||
content.contains("```") ||
content.contains("](")
} }
object CachedUrlParser { object CachedUrlParser {
@@ -136,7 +136,11 @@ fun RichTextViewer(
nav: INav, nav: INav,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
if (remember(content) { CachedRichTextParser.isMarkdown(content) }) { val state =
remember(content, tags) {
CachedRichTextParser.parseText(content, tags, callbackUri, authorPubKey)
}
if (state.isMarkdown) {
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav) RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
} else { } else {
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, authorPubKey, accountViewModel, nav) RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, authorPubKey, accountViewModel, nav)
@@ -224,6 +224,7 @@ class RichTextParser {
customEmoji = emojiMap.toImmutableMap(), customEmoji = emojiMap.toImmutableMap(),
paragraphs = segments, paragraphs = segments,
tags = tags, tags = tags,
isMarkdown = isMarkdown(content),
) )
} }
@@ -411,6 +412,18 @@ class RichTextParser {
} }
companion object { companion object {
// Cheap heuristic: stored on the parsed state so callers (e.g. RichTextViewer's
// markdown vs regular dispatch) can read the decision off the cached result instead
// of running a separate scan + maintaining a separate cache.
fun isMarkdown(content: String): Boolean =
content.startsWith("> ") ||
content.startsWith("# ") ||
content.contains("##") ||
content.contains("__") ||
content.contains("**") ||
content.contains("```") ||
content.contains("](")
val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$") val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$")
val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$") val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$")
@@ -33,6 +33,7 @@ class RichTextViewerState(
val customEmoji: ImmutableMap<String, String>, val customEmoji: ImmutableMap<String, String>,
val paragraphs: ImmutableList<ParagraphState>, val paragraphs: ImmutableList<ParagraphState>,
val tags: ImmutableListOfLists<String>, val tags: ImmutableListOfLists<String>,
val isMarkdown: Boolean = false,
) )
@Immutable @Immutable