60edd473c7
- 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.0 KiB
4.0 KiB
Image Loading
Amethyst uses Coil 3.x (KMP) for async image loading on both Android and Desktop. Coil is configured once at app startup with custom fetchers, decoders, and an OkHttp/Ktor network layer wired to Tor/proxy settings.
Android Setup
Under amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/:
ImageLoaderSetup.kt—class ImageLoaderSetupis the entry point. Called fromAmethyst.onCreate(). Installs a global CoilImageLoaderwith custom fetchers/decoders and a sharedOkHttpClient(viaOkHttpFactory).ImageCacheFactory.kt— builds the disk + memory cache backing the loader. Size-bounded; cleared on memory pressure.ThumbnailDiskCache.kt— separate on-disk cache for generated thumbnails.Base64Fetcher.kt— resolvesdata:image/...;base64,...URLs into bitmaps inline.BlossomFetcher.kt— fetches blossom-hosted media using authenticated requests (NIP-96 / blossom auth event).BlurHashFetcher.kt/ThumbHashFetcher.kt— synthesize placeholders fromblurhash/thumbhashin NIP-92imetatags while the real image loads.ProfilePictureFetcher.kt— special-case fetcher that falls back to a robohash when a profile has nopicture.MyDebugLogger(insideImageLoaderSetup.kt) — surfaces loader errors to logcat in debug builds.
Desktop Setup
Mirror under desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/images/:
DesktopImageLoaderSetup.kt— same shape as Android, but uses Skia decoders and a JVM-native OkHttp client.DesktopBase64Fetcher.kt,DesktopBlurHashFetcher.kt,DesktopThumbHashFetcher.kt,SkiaGifDecoder.kt— Skia / JVM equivalents of the Android fetchers.- Called from
desktopApp/.../desktop/Main.kt(fun main()at L172) viaDesktopImageLoaderSetup.setup()beforeapplication { }.
Composable Entry Points (Android)
amethyst/.../ui/components/MyAsyncImage.kt—@Composable fun MyAsyncImage(...). Wraps Coil'sAsyncImagewith the project's error fallbacks, blurhash placeholders, and content-description defaults.amethyst/.../ui/components/RobohashAsyncImage.kt— auto-generates a deterministic robohash avatar from a pubkey when no profile picture is set.amethyst/.../ui/components/ImageGallery.kt— paged/zoomable gallery for notes with multiple images.
Desktop has parallel composables under desktopApp/.../ui/ — they consume the shared Coil ImageLoader configured by DesktopImageLoaderSetup.
Typical Reuse
MyAsyncImage(
model = url,
contentDescription = description,
modifier = Modifier.size(48.dp).clip(CircleShape),
placeholderBlurHash = imeta?.blurhash, // NIP-92 placeholder
loading = { /* shimmer */ },
error = { /* broken image */ },
)
For profile pictures, prefer RobohashAsyncImage so users without a picture field still get a stable avatar.
Gotchas
- Never call
ImageLoader.Builderin a composable — build once inImageLoaderSetupand rely onSingletonImageLoader. Otherwise you shred the cache and blow up memory. - Blossom/encrypted media must go through
BlossomFetcher— using the default HTTP fetcher returns ciphertext that decoders reject. - Debug logcat noise:
MyDebugLoggeris on in debug builds only; do not enable in release. OkHttpFactoryfeeds the loader — if you replace the HTTP client (e.g. to route through Tor), update the factory, not the loader setup, or Tor routing silently bypasses images.- NIP-92
imetametadata flows in from the event, not the URL. Seecompose-expert/references/rich-text-parsing.mdfor howMediaUrlImage/MediaUrlVideocarry the blurhash and dimensions into the composable.
Related
compose-expert/references/rich-text-parsing.md— howMediaContentModelsfeed composables.gradle-expert/references/version-catalog-guide.md— Coil version alignment (coil = 3.4.0inlibs.versions.toml).