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:
Claude
2026-04-21 21:00:45 +00:00
parent 9147f1b08b
commit 60edd473c7
31 changed files with 1743 additions and 373 deletions
@@ -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