mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
§8 of the interop plan ends with a requirement: the metadata-exposure analysis "should be surfaced in the UI if we ship this, not buried. A Marmot group and a cordn group have materially different metadata exposure and users cannot infer that from either one looking like a group chat." GroupExposure existed so there would be something to render; this renders it. The disclosure sits where it can still change a decision — on a pasted cordn1… link, before joining, rather than in a settings page nobody opens. The screen reads the link, names the coordinator and its relays, and shows the exposure. It deliberately does not join: joining needs a live coordinator and Tier B is blocked on licensing (§7), so a join button would be half a feature whose other half has never been run. - commonsUI/.../cordn/ui/: CordnExposureCard, CoordinatorHealthRow, CordnGroupBadge. Material3 only, so Desktop gets them for free. - commons/.../CordnLinkInspection: parse-and-disclose, headless and tested, so the screen only draws. A ref naming no coordinator is valid but not followable, and carries no exposure — inventing a threat model for a server we cannot identify would be worse than saying nothing. - Settings → Cordn group link, with a route and search keywords. Rendering it found two bugs compiling could not: - The severity colours were not monotonic. `tertiary` for the middle level rendered pink against a dark `onSurface` for the worst one, so "under a throwaway key" read as more alarming than "tied to your real account". Emphasis for the worst case is now weight rather than another hue. Nothing uses `error`: everything on the card is how cordn works, not a fault, and painting normal operation red teaches people to ignore red. - Note order is part of the disclosure. Cross-group linkage (§8.2), the item nobody predicts, sat below message padding, the least consequential one. notes() now returns most-surprising-first. CordnExposureRenderTest rasterises the surface headlessly (ImageComposeScene, software Skia, no display, no new dependency — the same Compose Desktop artifact jvmMain already uses) and asserts on pixels. It catches what a compile cannot: a missing string resource, which throws at render because the generated Res.string.* accessors compile regardless, and a composable that draws nothing. Mutation-checked — hardcoding the card background and disabling the §8.2 conditional each kill exactly one test. The first version of the theme test sampled only the page background and let the hardcoded card through, so it now samples inside the card as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
240 lines
9.4 KiB
Kotlin
240 lines
9.4 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)
|
|
|
|
// LruCache (KMP-ready)
|
|
implementation(libs.androidx.collection)
|
|
|
|
// Immutable collections
|
|
api(libs.kotlinx.collections.immutable)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
getByName("jvmMain").dependsOn(skikoMain)
|
|
iosMain.dependsOn(skikoMain)
|
|
|
|
getByName("androidHostTest") {
|
|
dependencies {
|
|
implementation(libs.junit)
|
|
}
|
|
}
|
|
|
|
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"))
|