Files
amethyst/commonsUI/ARCHITECTURE.md
T
Claude 150177f5a0 fix(strings): move the cordn and backup strings to commonsUI, and write the rule down
The 2026-09-22 move left Android res/ with only the ~190 strings a platform
API reads synchronously. The cordn UI and the backup-conflict review then
added 447 keys back to it. All of them move with tools/strings-migrate
(1,131 elements across the locales that translate them), and the 18 files
that read them now use Res.string/Res.plurals.

The parts that were not a rename:
- stringRes(context, ...) in the suspend attachment/voice senders becomes
  loadStringRes; adminFailureText becomes suspend (its only caller is already
  inside scope.launch).
- The backup review held labels as @StringRes Int (DiffGroup.label, the
  ReviewRow *Res fields, eventTypeName/Explainer, the tile/hero/lane params).
  They are StringResource / PluralStringResource now.
- rememberPresentation resolved labels inside remember {} through
  LocalResources. Compose resources have no blocking accessor, so the 16
  labels presentationOf writes into item text are resolved in composition
  into a map, and that map is the remember key.
- androidx pluralStringResource -> the compose-resources one.

The rule is now in .claude/CLAUDE.md ("Strings"), commonsUI/ARCHITECTURE.md
and the android-expert and find-missing-translations skills: new strings go
in commonsUI composeResources, Android-only screens included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY
2026-09-26 21:13:01 +00:00

4.0 KiB

commonsUI — the Compose half of the shared layer

commonsUI holds every piece of shared code that needs Compose UI and is therefore useless to the headless cli:

  • composables (ui/<area>, <feature>/ui — e.g. nip64Chess/ui, profile/ui — and the historical flat audio renderers),
  • ImageVector icons (icons, hashtags, robohash) and the icon fonts,
  • theme, layouts, markdown rendering (ui/markdown, jvmAndroid),
  • Coil (service/image: CoilImageBridge + BlurHash/ThumbHash/Base64/Blossom fetchers),
  • the composeResources tree (translated strings, fonts, the napplet shell + shim files) and the generated com.vitorpamplona.amethyst.commons.resources.Res,
  • the @Composable relay-client entry points (observeUser*, *FilterAssemblerSubscription, KeyDataSourceSubscription),
  • state holders that carry a foundation/ui type (LevelFeedViewModel with its LazyListState, ChatNewMessageState with TextFieldValue, EmojiSuggestionState with TextFieldState).

It depends on :commons (and :quartz) as api, so a consumer that adds :commonsUI sees the headless layer transitively. amethyst, desktopApp, nappletHost and benchmark depend on it; cli, geode, marmotBench must never.

Same packages as commons

Every file keeps its com.vitorpamplona.amethyst.commons.* package. The split is a build-graph boundary, not a package rename: nothing in the apps had to change an import, and a file can move between the two modules with a git mv. The rule for which module a file lives in is mechanical:

imports androidx.compose.ui / foundation / material3 / animation, Coil, org.jetbrains.compose.resources, Res, org.jetbrains.skia, or declares a @Composable → commonsUI. Otherwise → commons.

The only cross-module gotcha is internal: a commonsUI file cannot see an internal declaration in commons. Make it public (or move the caller).

The full package taxonomy — including which packages are "mixed" (logic in commons, composables here, same package name) — is the table in commons/ARCHITECTURE.md §2. Read that first; this file only documents what is specific to the UI module.

Source sets

Source set For
commonMain Composables, icons, theme, Coil fetchers, composeResources. Gated by verifyKmpPurity like commons.
jvmAndroid Markdown renderer (ui/markdown), Coil-OkHttp + Blossom read-auth fetcher, the viewModel() helper, and the avatar/image engine (ui/components/RobohashAsyncImage.kt: RobohashFallbackAsyncImage, GifProfilePicture) behind the UserAvatar AvatarImage actual. For a user avatar call the commonMain UserAvatar; don't add another avatar composable.
jvmMain Desktop Coil bridge (CoilImageBridge.jvm.kt), a no-op AnimatedImageAutoPlay (Coil decodes only the first frame), compose.desktop.currentOs. dependsOn(jvmAndroid) + skikoMain.
androidMain Android Coil bridge, the AnimatedImageAutoPlay actual that starts/stops GIF/AVIF drawables. dependsOn(jvmAndroid).
skikoMain org.jetbrains.skia pixel helpers shared by desktop JVM + iOS (SkiaBitmapConverter).
iosMain iOS Coil bridge, a plain AsyncImage AvatarImage actual. Compile-only spike today.

Tooling that points here

  • Icon fonts: tools/material-symbols-subset/subset.sh and tools/icon-font/build_icon_font.py read icons/ and write composeResources/font/ in this module (see root .claude/CLAUDE.md, "Icons").
  • Translations: crowdin.yml and tools/strings-migrate/ target commonsUI/src/commonMain/composeResources/. This is where every new user-visible string goes, Android-only screens included; the app's own res/values/strings.xml holds only the synchronous-platform tier (see root .claude/CLAUDE.md, "Strings").
  • CI: .github/workflows/build.yml runs :commonsUI:jvmTest, :commonsUI:verifyKmpPurity and the iOS compile/test tasks next to the :commons ones.