Files
amethyst/commonsUI/build.gradle.kts
Claude d71fcf0379 fix: keep modal-sheet behavior and empty d-tag handling from the warning cleanup
Audit of the warning-cleanup commit found two regressions in it:

- rememberModalSheetState went through rememberBottomSheetState, which is
  not equivalent to the deprecated rememberModalBottomSheetState: the old
  function pins isBottomSheetPartiallyExpandedDeterministicEnabled to false,
  the new one takes the global flag (true by default), changing where sheets
  that keep PartiallyExpanded place and settle that anchor. The flag is only
  reachable through Material3 internals, so the helper now calls the
  deprecated function once, with a documented suppression; every sheet still
  goes through the helper.

- The Cashu cleanups dropped `?: event.id` fallbacks the compiler proved
  dead, but they were dead because dTag() returns "" for a missing tag, not
  because the tag is always there. d-tag-less kind:38000 recommendations and
  mint announcements all shared the key "". Use ifEmpty so they stay apart,
  skip "" in the d-tag -> URL map, and fall back to the event id for the
  recommendation card's title and the delete dialog.

Also declares navigationevent-compose (Apache-2.0, already in the graph via
navigation3-ui) directly in commonsUI skikoMain, since PlatformBackHandler
imports it, and corrects the gradle.properties note on what
kotlin.native.ignoreDisabledTargets silences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SazxgvMHyaPGET4ishFuTM
2026-10-08 02:32:26 +00:00

286 lines
12 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.DisableCacheInKotlinVersion
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCacheApi
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable
// `:commonsUI` is the Compose half of the shared layer: every composable,
// icon, theme, Coil fetcher and Compose-resource (strings/fonts/files) that the
// GUI front ends (Android, Desktop, iOS) render. It sits on top of `:commons`
// (headless models, state, ViewModels, relay client) and is never a dependency
// of `:cli`, which keeps Compose UI + Skiko off the CLI classpath.
// Source files keep their `com.vitorpamplona.amethyst.commons.*` packages so
// the module boundary is purely a build-graph constraint — consumers did not
// have to change a single import when the split happened.
// Disables the Kotlin/Native compiler cache for an iOS test binary so the
// Compose ui-uikit klib recompiles fresh instead of linking the broken prebuilt
// cache (see the call site in the `kotlin {}` block). The version guard makes
// Kotlin re-surface this workaround once we move past 2.4.20, so it can be
// dropped when a newer Compose/Kotlin pairing fixes the cache. Wrapped in a
// helper because @OptIn only applies to declarations, not bare statements.
@OptIn(KotlinNativeCacheApi::class)
fun TestExecutable.disableUiKitPrebuiltCache() =
disableNativeCache(
DisableCacheInKotlinVersion.`2_4_20`,
"Compose ui-uikit prebuilt cache references UIViewLayoutRegion (iOS 17+); " +
"linking the iOS test binary fails under Xcode 16.4.",
)
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary)
alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.composeMultiplatform)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
android {
namespace = "com.vitorpamplona.amethyst.commons.ui"
compileSdk =
libs.versions.android.compileSdk
.get()
.toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
androidResources.enable = true
withHostTest {
isReturnDefaultValues = true
}
withDeviceTest {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
// iOS targets — same compile-only spike as :commons.
iosArm64()
iosSimulatorArm64()
// Compose Multiplatform 1.11.x ships an `org.jetbrains.compose.ui:ui-uikit`
// prebuilt Kotlin/Native cache whose CMPLayoutRegion object hard-references
// the UIKit class `UIViewLayoutRegion` (introduced in iOS 17). Linking the
// iOS *test* executable against that cache under Xcode 16.4 fails with
// ld: Undefined symbols: _OBJC_CLASS_$_UIViewLayoutRegion
// because the cached object was built for a newer simulator SDK (18.5) than
// the test binary is being linked for (14.0). Disabling the native cache for
// the iOS test binaries makes ui-uikit recompile against the active SDK,
// where the symbol resolves. See disableUiKitPrebuiltCache() above and
// https://kotl.in/disable-native-cache
targets.withType<KotlinNativeTarget>().configureEach {
binaries.withType<TestExecutable>().configureEach {
disableUiKitPrebuiltCache()
}
}
sourceSets {
commonMain {
dependencies {
// The headless half. `api` because every composable here takes
// or returns commons types (Note, User, ViewModels, states).
api(project(":commons"))
api(project(":quartz"))
// Compose Multiplatform
implementation(libs.jetbrains.compose.ui)
implementation(libs.jetbrains.compose.foundation)
implementation(libs.jetbrains.compose.runtime)
implementation(libs.jetbrains.compose.material3)
implementation(libs.jetbrains.compose.ui.tooling.preview)
// Lifecycle (KMP since 2.8.0). lifecycle-runtime-compose ships
// iOS variants; lifecycle-viewmodel-compose (the viewModel()
// Composable helper) is Android-only and lives in jvmAndroid.
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
// Image loading (Coil 3 - KMP). The okhttp network fetcher is
// JVM-only and lives in jvmAndroid; iOS will pull coil-ktor
// when that target wires its actual.
implementation(libs.coil.compose)
// NetworkHeaders on avatar requests (the local Blossom cache marker). Multiplatform,
// Apache-2.0; already on the JVM classpath through coil-network-okhttp.
implementation(libs.coil.network.core)
// LruCache (KMP-ready)
implementation(libs.androidx.collection)
// Pinch-to-zoom (net.engawapg zoomable, Compose Multiplatform: Android, desktop,
// iOS; Apache-2.0). Already in the app; the Blossom blob viewer uses it here.
implementation(libs.zoomable)
// Charts (Vico 3, Compose Multiplatform, Apache-2.0): the notification
// summary builds its chart model off the main thread. `api` because the
// model is part of that state holder's public surface.
api(libs.vico.charts.compose)
// Immutable collections
api(libs.kotlinx.collections.immutable)
// JSON trees for the LNURL-pay replies the payment stack reads.
implementation(libs.kotlinx.serialization.json)
// Compose Multiplatform Resources (strings, fonts, napplet shell files)
implementation(libs.jetbrains.compose.components.resources)
// KMP syntax highlighter (Apache-2.0) for the git code browser.
implementation(libs.highlights)
// Navigation 3 (JetBrains' multiplatform build, Apache-2.0: Android, desktop, iOS).
// The app owns its back stack (navs/NavBackStacks); NavDisplay renders it, and the
// ViewModel decorator scopes each screen's ViewModels to its entry.
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
jvmTest {
dependencies {
// Compose Desktop on the test classpath so composables can be
// rendered headlessly to a Skia surface (ImageComposeScene) and
// asserted on. No new third-party dependency: this is the same
// artifact jvmMain already uses, and it rasterises in software,
// so it needs no display.
implementation(compose.desktop.currentOs)
// RouteForPointerTest stubs an Account (already the app's and androidHostTest's mock lib).
implementation(libs.mockk)
}
}
// Shared JVM code for both Android and Desktop
val jvmAndroid =
create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
// Coil's OkHttp network fetcher (JVM-only). iOS will use
// coil-ktor when the iOS Compose UI ships.
implementation(libs.coil.okhttp)
// OkHttp for the Blossom read-auth Coil fetcher.
implementation(libs.okhttp)
// executeAsync for PdfFetcher; already on every module that ships OkHttp.
implementation(libs.okhttpCoroutines)
// Markdown rendering (richtext-commonmark). The single
// consumer (RenderMarkdown.kt) lives in jvmAndroid.
// iOS support pending Phase 3 markdown decision.
implementation(libs.markdown.commonmark)
implementation(libs.markdown.ui)
implementation(libs.markdown.ui.material3)
// viewModel() Compose helper. AndroidX publishes this
// artifact for android/jvmStubs/linuxx64Stubs but not iOS,
// so it stays in jvmAndroid until we either swap to the
// org.jetbrains.androidx.lifecycle variant or accept a
// platform-specific ViewModel access pattern on iOS.
implementation(libs.androidx.lifecycle.viewmodel.compose)
// ZXing core encodes the QR codes the shared QrCodeDrawer shows. Pure Java,
// Apache-2.0, and already linked by both apps.
implementation(libs.zxing)
}
}
jvmMain {
dependsOn(jvmAndroid)
dependencies {
// Desktop-specific Compose
implementation(compose.desktop.currentOs)
implementation(libs.jetbrains.compose.ui.tooling)
}
}
androidMain {
dependsOn(jvmAndroid)
dependencies {
// Android-specific Compose tooling
implementation(libs.androidx.ui.tooling.preview)
// WindowInsetsControllerCompat, for DisappearingScaffold's immersive status bar.
implementation(libs.androidx.core.ktx)
// BackHandler, behind the shared PlatformBackHandler. Apache-2.0, already in the app.
implementation(libs.androidx.activity.compose)
// Camera permission prompt for TakePicture. Apache-2.0, already in the app.
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.adaptive)
// BiometricPrompt behind rememberDeviceAuthenticator. Apache-2.0, already in the app.
implementation(libs.androidx.biometric.ktx)
}
}
// iOS intermediate so iosArm64Main and iosSimulatorArm64Main share code.
val iosMain =
create("iosMain") {
dependsOn(commonMain.get())
}
getByName("iosArm64Main").dependsOn(iosMain)
getByName("iosSimulatorArm64Main").dependsOn(iosMain)
// Skiko-backed targets (desktop JVM + iOS) share pixel-format helpers
// (org.jetbrains.skia.* resolves on both through Compose). Android is
// deliberately NOT in this set — it renders through android.graphics.
val skikoMain =
create("skikoMain") {
dependsOn(commonMain.get())
dependencies {
// NavigationBackHandler for desktop (Esc) and iOS (swipe), behind
// PlatformBackHandler. Apache-2.0; Android uses AndroidX activity's BackHandler.
implementation(libs.jetbrains.navigationevent.compose)
}
}
getByName("jvmMain").dependsOn(skikoMain)
iosMain.dependsOn(skikoMain)
getByName("androidHostTest") {
dependencies {
implementation(libs.junit)
// SafeImeInsetsTest mocks the android.view.View its per-window cache is keyed on.
implementation(libs.mockk)
implementation(libs.kotlinx.coroutines.test)
}
}
getByName("androidDeviceTest") {
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.espresso.core)
}
}
}
}
compose.resources {
publicResClass = true
// Kept on the pre-split package so `Res` imports in every consumer
// (amethyst, desktopApp, nappletHost) keep resolving unchanged.
packageOfResClass = "com.vitorpamplona.amethyst.commons.resources"
generateResClass = always
}
// iOS purity gate — shared task, see gradle/kmp-purity.gradle.kts.
apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))