- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20). - Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references. - Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching. - Removes compose-desktop.md (superseded by desktop-expert/). - Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption, large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing, android-expert/image-loading. - New skills: account-state (Account + LocalCache), relay-client (subscriptions, filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family), auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
4.1 KiB
Rich Text Parsing
Amethyst converts raw event content (plain text with URLs, mentions, hashtags, media links, nostr references, markdown) into structured segments that Compose can render. Everything lives under commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/.
Files
RichTextParser.kt— the main entry point. Aclass RichTextParserthat takes a note's content, the URL preview cache, and NIP-92imetatags and returns aRichTextViewState.RichTextParserSegments.kt— segment data classes (hashtag, url, mention, invoice, etc.) that the parser emits.Patterns.kt— the regex bank. Single source of truth for URL, hashtag, mention, email, invoice, cashu, nostr-URI patterns. Prefer adding a case here to writing a one-off regex at a call site.UrlParser.kt— URL extraction + validation; used to pull URLs out of free-form text before the parser classifies them.GalleryParser.kt— buildsMediaGallerygroupings from consecutive media URLs in a note.MediaContentModels.kt— the rendering contracts:MediaUrlImage,MediaUrlVideo— plain HTTP(S) media with optional NIP-92 metadata.EncryptedMediaUrlImage,EncryptedMediaUrlVideo— for encrypted/blossom-gated media.MediaLocalImage,MediaLocalVideo— for drafts / not-yet-uploaded media.
Base64Image.kt— inline base64 data URI support.ExpandableTextCutOffCalculator.kt— decides where to truncate long content for "Show more" fold points.
How a Note Becomes Rendered UI
- Raw
content: Stringarrives (from anEvent). RichTextParserscans with patterns, extracts URLs, nostr IDs, hashtags, mentions, invoices, cashu tokens.- URLs are classified against
imetatags (NIP-92) so media gets correct dimensions, mime type, blurhash. GalleryParsergroups adjacent media into a singleMediaGallerysegment.- The composable layer (elsewhere in
commons/compose/and amethyst/desktop UI) walks the segment list and renders each with the appropriate composable (RenderMarkdown,NoteQuoteBody,ClickableUrl, etc.).
Typical Reuse
// inside a composable
val state = remember(note, imetaTags) {
CachedRichTextParser.parseReturningNullable(content, imetaTags, callbackUri)
}
state?.paragraphs?.forEach { paragraph ->
paragraph.words.forEach { segment ->
when (segment) {
is UrlSegment -> ClickableUrl(segment)
is HashtagSegment -> HashtagChip(segment)
is NostrRefSegment -> NoteCompose(segment.entity)
is ImageSegment -> ZoomableMedia(segment.media)
// ...
}
}
}
On Android there's amethyst/.../service/CachedRichTextParser.kt which caches parser output per content — re-parsing the same note on every recomposition is expensive, so always parse behind a cache.
NIP-92 imeta Enrichment
imeta tags attached to an event carry structured metadata for each media URL: url, m (mime), dim, blurhash, x (sha256), size. RichTextParser maps these into MediaUrlImage / MediaUrlVideo so the renderer can reserve correct aspect ratio and show a blurhash placeholder before the image loads. Reference: nip-catalog.md.
Gotchas
- Don't parse on every recomposition. Use
CachedRichTextParser(Android) orremember(content, imeta) { … }for commonMain. - Regexes live in
Patterns.kt. If you're writing a new regex for URLs/mentions/hashtags in a UI file, move it toPatterns.ktinstead. - Segments are
@Immutabledata classes — safe to pass to Compose without triggering recomposition spam. - Encrypted media is a separate class (
EncryptedMediaUrl*). If you handleMediaUrlImagebut not its encrypted sibling, blossom/NIP-17 gated media silently falls through. GalleryParsergroups across whitespace-only-lines between URLs. Changing its grouping rules breaks layout in many note screens.
Related
nostr-expert/references/nip-catalog.md— NIP-92 (imeta) spec locationcompose-expert/references/shared-composables-catalog.md— which composables consume which segment types