Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- 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).
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# 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. A `class RichTextParser` that takes a note's content, the URL preview cache, and NIP-92 `imeta` tags and returns a `RichTextViewState`.
|
||||
- **`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`** — builds `MediaGallery` groupings 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
|
||||
|
||||
1. Raw `content: String` arrives (from an `Event`).
|
||||
2. `RichTextParser` scans with patterns, extracts URLs, nostr IDs, hashtags, mentions, invoices, cashu tokens.
|
||||
3. URLs are classified against `imeta` tags (NIP-92) so media gets correct dimensions, mime type, blurhash.
|
||||
4. `GalleryParser` groups adjacent media into a single `MediaGallery` segment.
|
||||
5. 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
|
||||
|
||||
```kotlin
|
||||
// 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) or `remember(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 to `Patterns.kt` instead.
|
||||
- **Segments are `@Immutable`** data classes — safe to pass to Compose without triggering recomposition spam.
|
||||
- **Encrypted media is a separate class** (`EncryptedMediaUrl*`). If you handle `MediaUrlImage` but not its encrypted sibling, blossom/NIP-17 gated media silently falls through.
|
||||
- **`GalleryParser` groups** 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 location
|
||||
- `compose-expert/references/shared-composables-catalog.md` — which composables consume which segment types
|
||||
Reference in New Issue
Block a user