finish skills

This commit is contained in:
nrobi144
2025-12-30 15:45:02 +02:00
parent f654af9d8a
commit f180fd39e1
18 changed files with 5287 additions and 42 deletions
+577
View File
@@ -0,0 +1,577 @@
---
name: compose-expert
description: Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
---
# Compose Multiplatform Expert
Visual UI patterns for sharing composables across Android and Desktop.
## When to Use This Skill
- Creating or refactoring shared UI components
- Deciding whether to share UI in `commonMain` or keep platform-specific
- Building custom ImageVector icons (robohash pattern)
- State management: remember, derivedStateOf, produceState
- Recomposition optimization: visual usage of @Stable/@Immutable
- Material3 theming and styling
- Performance: lazy lists, image loading
**Delegate to other skills:**
- Navigation structure → `android-expert`, `desktop-expert`
- Kotlin state patterns (StateFlow, sealed classes) → `kotlin-expert`
- Build configuration → `gradle-expert`
## Philosophy: Share by Default
**Default to `commons/commonMain`** unless platform experts indicate otherwise.
### Always Share
- **UI components**: Buttons, cards, lists, dialogs, inputs
- **State visualization**: Loading, empty, error states
- **Custom icons**: ImageVector assets (robohash, custom paths)
- **Theme utilities**: Color calculations, style helpers
- **Material3 components**: Any UI using Material primitives
### Keep Platform-Specific
- **Navigation structure**: Bottom nav (Android) vs Sidebar (Desktop)
- **Screen layouts**: Platform-specific scaffolding
- **System integrations**: File pickers, notifications, share sheets
- **Platform UX**: Gestures, keyboard shortcuts, window management
### Decision Framework
1. **Uses only Material3 primitives?** → Share in `commonMain`
2. **Requires platform system APIs?** → Platform-specific
3. **Pure visual component without navigation?** → Share in `commonMain`
4. **Needs platform UX patterns?** → Ask `android-expert` or `desktop-expert`
If uncertain, **default to sharing** - easier to split later than merge.
## Shared Composable Anatomy
### Structure
```kotlin
@Composable
fun SharedComponent(
// State parameters (read-only)
data: DataClass,
isLoading: Boolean,
// Event parameters (write-only)
onAction: () -> Unit,
// Visual parameters
modifier: Modifier = Modifier,
// Optional customization
colors: ComponentColors = ComponentDefaults.colors()
) {
// Implementation
}
```
**Pattern**: State down, events up
- Parameters above modifier = required state/events
- `modifier` parameter = layout control
- Parameters below modifier = optional customization
### Example: AddButton
```kotlin
@Composable
fun AddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Add",
enabled: Boolean = true
) {
OutlinedButton(
modifier = modifier,
enabled = enabled,
onClick = onClick,
shape = ActionButtonShape,
contentPadding = ActionButtonPadding
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
// Shared constants for consistency
val ActionButtonShape = RoundedCornerShape(20.dp)
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
```
**Why this works on all platforms:**
- Material3 primitives (OutlinedButton, Text)
- No platform APIs
- Configurable through parameters
- Consistent styling via shared constants
## State Management Patterns
### remember - Cache Across Recompositions
```kotlin
@Composable
fun ExpandableCard() {
var isExpanded by remember { mutableStateOf(false) }
Column {
IconButton(onClick = { isExpanded = !isExpanded }) {
Icon(
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Collapse" else "Expand"
)
}
if (isExpanded) {
Text("Expanded content...")
}
}
}
```
**Visual pattern**: Toggle button → state changes → UI expands/collapses
**Use for**: Simple UI state (toggles, counters, text input)
### derivedStateOf - Optimize Frequent Changes
```kotlin
@Composable
fun ScrollToTopButton(listState: LazyListState) {
// Only recomposes when showButton changes, not every scroll pixel
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
```
**Visual pattern**: Scroll position (0, 1, 2...) → boolean (show/hide) → Button visibility
**Use for**: Input changes frequently, derived result changes rarely
**Performance**: Prevents recomposition on every scroll event
### produceState - Async to Compose State
```kotlin
@Composable
fun LoadUserProfile(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = repository.fetchUser(userId)
}
}
@Composable
fun ProfileScreen(userId: String) {
val user by LoadUserProfile(userId)
when (user) {
null -> LoadingState("Loading profile...")
else -> ProfileCard(user!!)
}
}
```
**Visual pattern**: Async operation → state updates → UI reflects changes
**Use for**: Convert Flow, LiveData, callbacks into Compose state
**Lifecycle**: Coroutine cancelled when composable leaves composition
For Kotlin-specific state patterns (StateFlow, sealed classes), see `kotlin-expert`.
## State Hoisting
Move state up to make composables reusable:
```kotlin
// ❌ Stateful - hard to test, can't control externally
@Composable
fun BadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ Stateless - reusable, testable
@Composable
fun GoodSearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier
)
}
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
Column {
GoodSearchBar(query = query, onQueryChange = { query = it })
SearchResults(query = query)
}
}
```
**Principle**: State up, events down
- State: `query: String` (read-only parameter)
- Events: `onQueryChange: (String) -> Unit` (callback parameter)
## Recomposition Optimization
### Visual Usage of @Immutable
Use @Immutable on data classes passed to composables:
```kotlin
@Immutable
data class UserProfile(val name: String, val avatar: String)
@Composable
fun ProfileCard(profile: UserProfile) {
// Only recomposes when profile instance changes
Row {
RobohashImage(robot = profile.avatar)
Text(profile.name, style = MaterialTheme.typography.titleMedium)
}
}
```
**Visual effect**: Prevents recomposition when parent recomposes with same data
**Pattern**: Mark parameter data classes as @Immutable
**Note**: For Kotlin language details on @Immutable, see `kotlin-expert`
### Stable Parameters
```kotlin
// ✅ Stable - won't trigger recomposition unless colors instance changes
@Composable
fun ThemedCard(
content: String,
colors: CardColors = CardDefaults.colors(),
modifier: Modifier = Modifier
) {
Card(colors = colors, modifier = modifier) {
Text(content)
}
}
```
For @Stable annotation details, see `kotlin-expert`.
## Material3 Theming
All shared composables use Material3 for consistency:
```kotlin
@Composable
fun ThemedComponent() {
val bg = MaterialTheme.colorScheme.background
val fg = MaterialTheme.colorScheme.onBackground
val primary = MaterialTheme.colorScheme.primary
Column(
modifier = Modifier.background(bg)
) {
Text(
"Title",
style = MaterialTheme.typography.headlineMedium,
color = fg
)
Button(
onClick = { /* ... */ },
colors = ButtonDefaults.buttonColors(containerColor = primary)
) {
Text("Action")
}
}
}
```
**Principles:**
- Colors: `MaterialTheme.colorScheme.*`
- Typography: `MaterialTheme.typography.*`
- Shapes: `MaterialTheme.shapes.*`
### Theme Detection
```kotlin
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
return (background.red + background.green + background.blue) / 3 > 0.5f
}
@Composable
fun ThemedIcon() {
val isDark = !isLightTheme()
val tint = if (isDark) Color.White else Color.Black
Icon(Icons.Default.Face, null, tint = tint)
}
```
## Custom Icons: ImageVector Pattern
Amethyst uses ImageVector for multiplatform icons.
### roboBuilder DSL
```kotlin
fun roboBuilder(block: Builder.() -> Unit): ImageVector {
return ImageVector.Builder(
name = "Robohash",
defaultWidth = 300.dp,
defaultHeight = 300.dp,
viewportWidth = 300f,
viewportHeight = 300f
).apply(block).build()
}
```
### Building Icons
```kotlin
fun customIcon(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
builder.addPath(pathData3, fill = Black, fillAlpha = 0.2f)
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
lineToRelative(16.0f, 16.0f)
close()
}
@Composable
fun CustomIcon() {
Image(
painter = rememberVectorPainter(
roboBuilder {
customIcon(SolidColor(Color.Blue), this)
}
),
contentDescription = "Custom icon"
)
}
```
**Why ImageVector?**
- Pure Kotlin, no XML
- Works on Android, Desktop, iOS
- GPU-accelerated
- Type-safe
### Caching Pattern
```kotlin
object CustomIcons {
private val cache = mutableMapOf<String, ImageVector>()
fun get(key: String): ImageVector {
return cache.getOrPut(key) {
buildIcon(key)
}
}
}
@Composable
fun CachedIcon(key: String) {
Image(imageVector = CustomIcons.get(key), contentDescription = null)
}
```
For detailed icon patterns, see `references/icon-assets.md`.
## Common Visual Patterns
### State Visualization
```kotlin
@Composable
fun DataScreen(uiState: UiState) {
when (uiState) {
is UiState.Loading -> LoadingState("Loading...")
is UiState.Empty -> EmptyState(
title = "No data",
onRefresh = { /* refresh */ }
)
is UiState.Error -> ErrorState(
message = uiState.message,
onRetry = { /* retry */ }
)
is UiState.Success -> ContentList(uiState.items)
}
}
```
**Components** (all in `commons/commonMain`):
- `LoadingState` - Progress indicator + message
- `EmptyState` - Empty message + optional refresh button
- `ErrorState` - Error message + optional retry button
### Relay Status (Amethyst Pattern)
```kotlin
@Composable
fun RelayStatusIndicator(connectedCount: Int) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
tint = statusColor,
modifier = Modifier.size(16.dp)
)
Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
```
**Visual mapping**:
- 0 relays → Red + X icon
- 1-2 relays → Yellow + Check icon
- 3+ relays → Green + Check icon
### Placeholder Pattern
```kotlin
@Composable
fun PlaceholderScreen(
title: String,
description: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(title, style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
Text(description, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Specific implementations
@Composable
fun SearchPlaceholder() = PlaceholderScreen(
title = "Search",
description = "Search for users, notes, and hashtags."
)
```
**Pattern**: Generic composable + specific wrappers with preset text
## Performance
### Avoid Unnecessary Recomposition
```kotlin
// ❌ Bad - recomposes on every scroll
@Composable
fun BadButton(scrollState: ScrollState) {
if (scrollState.value > 100) {
Button(onClick = {}) { Text("Top") }
}
}
// ✅ Good - only recomposes when visibility changes
@Composable
fun GoodButton(scrollState: ScrollState) {
val show by remember { derivedStateOf { scrollState.value > 100 } }
if (show) {
Button(onClick = {}) { Text("Top") }
}
}
```
### Lazy Lists
```kotlin
@Composable
fun FeedList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
FeedItem(item)
}
}
}
```
**Key principle**: Use `key` parameter for stable item identity
## Bundled Resources
- **references/shared-composables-catalog.md** - Complete catalog of shared UI components
- **references/state-patterns.md** - State management patterns with visual examples
- **references/icon-assets.md** - Custom ImageVector icon patterns
- **scripts/find-composables.sh** - Find all @Composable functions in codebase
## Quick Reference
| Task | Pattern | Location |
|------|---------|----------|
| Reusable UI | State hoisting | commons/commonMain |
| Simple state | remember { mutableStateOf() } | Composable scope |
| Derived state | derivedStateOf { } | remember block |
| Async → state | produceState { } | Composable function |
| Custom icons | roboBuilder + PathData | commons/icons |
| Loading/Error | LoadingState, ErrorState | commons/ui/components |
| Theme colors | MaterialTheme.colorScheme | Any @Composable |
| Navigation | Delegate to platform expert | amethyst/, desktopApp/ |
## Common Workflows
### Creating a Shared Component
1. Start in `commons/src/commonMain/kotlin/.../ui/components/`
2. Use Material3 primitives only
3. Hoist state (parameters for data, callbacks for events)
4. Add modifier parameter
5. Use MaterialTheme for colors/typography
6. Test on both Android and Desktop
### Converting Existing Component
1. Read current implementation in `amethyst/` or `desktopApp/`
2. Identify pure visual logic (no platform APIs)
3. Create in `commons/commonMain` with hoisted state
4. Replace platform implementations with shared component
5. Keep platform-specific wrappers if needed
### Custom Icon
1. Export SVG from design tool
2. Convert to PathData using Android Studio
3. Create icon function with roboBuilder
4. Add caching if generated dynamically
5. Wrap in @Composable for easy use
### Navigation (Delegate)
For navigation patterns:
- Android bottom nav → `android-expert`
- Desktop sidebar → `desktop-expert`
- Multi-window → `desktop-expert`
## Related Skills
- **kotlin-expert** - Kotlin language aspects (@Immutable details, StateFlow, sealed classes)
- **android-expert** - Android navigation, platform APIs
- **desktop-expert** - Desktop navigation, window management, OS specifics
- **kotlin-coroutines** - Async patterns, Flow integration
@@ -0,0 +1,365 @@
# Custom Icon Assets and ImageVector Patterns
Guide to creating and using custom ImageVector icons in Compose Multiplatform.
## Why ImageVector?
ImageVector is the native Compose format for vector graphics:
- **Pure Kotlin**: No XML, no asset files
- **Multiplatform**: Works on Android, Desktop, iOS without conversion
- **Performant**: Lightweight, composable, GPU-accelerated
- **Type-safe**: Compile-time checking, no resource IDs
## Amethyst Pattern: Robohash
Amethyst generates deterministic avatars using ImageVector builders.
### Architecture
```
commons/robohash/
├── RobohashAssembler.kt # Main assembly logic
├── CachedRobohash.kt # Caching layer
└── parts/
├── Face0C3po.kt # Face variants (0-9)
├── Eyes2Single.kt # Eye variants (0-9)
├── Mouth3Grid.kt # Mouth variants (0-9)
├── Body2Thinnest.kt # Body variants (0-9)
└── Accessory7Antenna.kt # Accessory variants (0-9)
```
**Pattern**: 10 variants per feature × 5 features = 100,000+ unique combinations
### roboBuilder DSL
Custom ImageVector builder with sensible defaults:
```kotlin
fun roboBuilder(block: Builder.() -> Unit): ImageVector {
return ImageVector.Builder(
name = "Robohash",
defaultWidth = 300.dp,
defaultHeight = 300.dp,
viewportWidth = 300f,
viewportHeight = 300f
).apply(block).build()
}
```
**Usage**:
```kotlin
@Composable
fun CustomIcon() {
Image(
painter = rememberVectorPainter(
roboBuilder {
// Add paths here
}
),
contentDescription = "Custom icon"
)
}
```
### Path Building Pattern
```kotlin
fun face0C3po(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
builder.addPath(pathData5, fill = Black, fillAlpha = 0.2f)
builder.addPath(pathData6, stroke = Black, strokeLineWidth = 1.0f)
builder.addPath(pathData7, fill = Black, stroke = Black, fillAlpha = 0.2f, strokeLineWidth = 0.75f)
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
curveToRelative(0.0f, 0.0f, 0.0f, 27.0f, 5.0f, 42.0f)
reflectiveCurveToRelative(10.0f, 38.0f, 10.0f, 38.0f)
lineToRelative(16.0f, 16.0f)
// ...
close()
}
```
**Key elements**:
- `pathData` variables for path commands
- `addPath()` for each layer
- Parameterized colors (`fgColor`)
- Constant colors (`Black`)
- Alpha for shadows/highlights
### PathData DSL
Compose's PathData builder provides SVG-like commands:
| Command | Description | Example |
|---------|-------------|---------|
| `moveTo(x, y)` | Move pen without drawing | `moveTo(100f, 100f)` |
| `lineTo(x, y)` | Draw line to point | `lineTo(200f, 150f)` |
| `curveToRelative(...)` | Relative cubic Bézier | `curveToRelative(10f, 20f, 30f, 40f, 50f, 60f)` |
| `reflectiveCurveToRelative(...)` | Smooth curve | `reflectiveCurveToRelative(-51f, 3f, -53f, 55f)` |
| `horizontalLineTo(x)` | Horizontal line | `horizontalLineTo(250f)` |
| `verticalLineTo(y)` | Vertical line | `verticalLineTo(300f)` |
| `close()` | Close path | `close()` |
**Relative vs Absolute**:
- `moveTo` / `lineTo` - Absolute coordinates
- `moveToRelative` / `lineToRelative` - Relative to current position
## Creating Custom Icons
### Method 1: From SVG (Recommended)
1. **Export SVG** from design tool (Figma, Illustrator)
2. **Convert to ImageVector** using Android Studio's Vector Asset tool
3. **Extract path data** and adapt to roboBuilder pattern
```kotlin
// SVG path: M 10 10 L 20 20 ...
// Becomes:
private val myIconPath = PathData {
moveTo(10f, 10f)
lineTo(20f, 20f)
// ...
}
```
### Method 2: Programmatic
Build paths programmatically for simple shapes:
```kotlin
fun simpleIcon(): ImageVector = roboBuilder {
addPath(
pathData = PathData {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(150f, 150f)
lineTo(50f, 150f)
close()
},
fill = SolidColor(Color.Blue),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f
)
}
```
### Method 3: Material Icons Extensions
Extend Material Icons when you need platform-consistent icons:
```kotlin
// For standard icons, use Material Icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
Icon(Icons.Default.Check, contentDescription = "Success")
Icon(Icons.Default.Close, contentDescription = "Error")
```
## CachedRobohash Pattern
Performance optimization for generated icons:
```kotlin
object CachedRobohash {
private val cache = mutableMapOf<Pair<String, Boolean>, ImageVector>()
fun get(seed: String, isLight: Boolean): ImageVector {
return cache.getOrPut(seed to isLight) {
RobohashAssembler.assemble(seed, isLight)
}
}
}
```
**Pattern**:
- Key: `(seed, theme)` pair
- Value: Assembled ImageVector
- Lifecycle: Application lifetime (never cleared)
**Usage**:
```kotlin
@Composable
fun RobohashImage(robot: String) {
Image(
imageVector = CachedRobohash.get(robot, isLightTheme()),
contentDescription = "Avatar for $robot"
)
}
```
## Color Management
### Dynamic Colors
Pass colors as parameters for theme adaptation:
```kotlin
fun themedIcon(fgColor: SolidColor, bgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = bgColor)
builder.addPath(pathData2, fill = fgColor)
}
@Composable
fun ThemedIcon() {
val fg = MaterialTheme.colorScheme.primary
val bg = MaterialTheme.colorScheme.surface
Image(
painter = rememberVectorPainter(
roboBuilder {
themedIcon(SolidColor(fg), SolidColor(bg), this)
}
),
contentDescription = null
)
}
```
### Static Colors
Define constants for colors that don't change:
```kotlin
val Black = SolidColor(Color.Black)
val White = SolidColor(Color.White)
val Transparent = SolidColor(Color.Transparent)
```
## Advanced Techniques
### Layering
Build complex icons with multiple layers:
```kotlin
fun complexIcon(builder: Builder) {
// Layer 1: Background
builder.addPath(bgPath, fill = SolidColor(Color.White))
// Layer 2: Shadow
builder.addPath(shadowPath, fill = SolidColor(Color.Black), fillAlpha = 0.2f)
// Layer 3: Main shape
builder.addPath(mainPath, fill = SolidColor(Color.Blue))
// Layer 4: Highlight
builder.addPath(highlightPath, fill = SolidColor(Color.White), fillAlpha = 0.3f)
// Layer 5: Stroke
builder.addPath(outlinePath, stroke = SolidColor(Color.Black), strokeLineWidth = 1f)
}
```
**Render order**: Bottom to top (first addPath = bottom layer)
### Alpha for Visual Effects
```kotlin
// Shadow
builder.addPath(shadowPath, fill = Black, fillAlpha = 0.4f)
// Highlight
builder.addPath(highlightPath, fill = White, fillAlpha = 0.2f)
// Glass effect
builder.addPath(glassPath, fill = White, fillAlpha = 0.1f)
```
### Stroke Styles
```kotlin
// Outline only
builder.addPath(path, stroke = Black, strokeLineWidth = 1.5f)
// Fill + outline
builder.addPath(path, fill = fgColor, stroke = Black, strokeLineWidth = 1f)
// Dashed (not supported directly, use multiple segments)
```
## Composable Icon Pattern
Wrap ImageVector in a Composable for reusability:
```kotlin
@Composable
fun MyCustomIcon(
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified
) {
Image(
painter = rememberVectorPainter(myIconVector()),
contentDescription = "My custom icon",
modifier = modifier,
colorFilter = if (tint != Color.Unspecified) {
ColorFilter.tint(tint)
} else null
)
}
```
**Usage**:
```kotlin
MyCustomIcon(
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
```
## Best Practices
### DO
✅ Cache generated ImageVectors for performance
✅ Use PathData DSL for readability
✅ Parameterize colors for theme support
✅ Use Material Icons for standard icons
✅ Keep viewport size consistent (e.g., 300×300)
✅ Layer paths from back to front
✅ Use alpha for shadows and highlights
### DON'T
❌ Generate ImageVectors in @Composable without caching
❌ Hardcode theme-specific colors
❌ Create custom icons for standard Material icons
❌ Use extreme viewport sizes (stay 24-1000dp)
❌ Mix absolute and relative coordinates unnecessarily
❌ Forget to close() paths
## Icon Organization
### Structure
```
commons/icons/
├── CustomIcons.kt # Icon collection object
├── icons/
│ ├── Zap.kt # Lightning bolt
│ ├── Relay.kt # Relay indicator
│ └── Bitcoin.kt # Bitcoin symbol
└── builders/
└── IconBuilder.kt # Shared builder utilities
```
### Collection Object
```kotlin
object CustomIcons {
val Zap: ImageVector by lazy { ZapIcon.create() }
val Relay: ImageVector by lazy { RelayIcon.create() }
val Bitcoin: ImageVector by lazy { BitcoinIcon.create() }
}
// Usage
Icon(CustomIcons.Zap, contentDescription = "Zap")
```
## Resources
- [Compose ImageVector API](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector)
- [SVG Path Commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths)
- [Material Icons](https://fonts.google.com/icons)
- Robohash implementation: `commons/robohash/` in AmethystMultiplatform
@@ -0,0 +1,281 @@
# Shared Composables Catalog
This catalog documents shared UI components in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/`.
## Directory Structure
```
commons/src/commonMain/kotlin/.../commons/ui/
├── components/ # Reusable UI components
├── screens/ # Screen-level composables
├── theme/ # Theming and styling
└── feed/ # Feed-specific components
```
## Components (`ui/components/`)
### State Visualization
**LoadingState** - Centered loading indicator with message
```kotlin
@Composable
fun LoadingState(message: String, modifier: Modifier = Modifier)
```
- Use for: Async operations, data fetching
- Pattern: fillMaxSize, centered Column, CircularProgressIndicator
- Works on: Android, Desktop
**EmptyState** - Centered empty state with optional refresh
```kotlin
@Composable
fun EmptyState(
title: String,
modifier: Modifier = Modifier,
description: String? = null,
onRefresh: (() -> Unit)? = null,
refreshLabel: String = "Refresh"
)
```
- Use for: Empty lists, no data scenarios
- Pattern: Centered Column, optional OutlinedButton
- Works on: Android, Desktop
**ErrorState** - Centered error message with retry
```kotlin
@Composable
fun ErrorState(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
retryLabel: String = "Try Again"
)
```
- Use for: Error handling, failed operations
- Pattern: error color, optional Button
- Works on: Android, Desktop
### Feed-Specific States
**FeedEmptyState** - Pre-configured empty state for feeds
```kotlin
@Composable
fun FeedEmptyState(
modifier: Modifier = Modifier,
title: String = "Feed is empty",
onRefresh: (() -> Unit)? = null
)
```
**FeedErrorState** - Pre-configured error state for feeds
```kotlin
@Composable
fun FeedErrorState(
errorMessage: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null
)
```
### Action Buttons
**Shared Constants**:
```kotlin
val ActionButtonShape = RoundedCornerShape(20.dp)
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
```
**AddButton** - Consistent "Add" action button
```kotlin
@Composable
fun AddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Add",
enabled: Boolean = true
)
```
- Pattern: OutlinedButton with consistent shape/padding
- Works on: Android, Desktop
**RemoveButton** - Consistent "Remove" action button
```kotlin
@Composable
fun RemoveButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Remove",
enabled: Boolean = true
)
```
### Custom Images
**RobohashImage** - Deterministic avatar generation
```kotlin
@Composable
fun RobohashImage(
robot: String, // Seed (e.g., pubkey)
modifier: Modifier = Modifier,
contentDescription: String? = null,
loadRobohash: Boolean = true
)
// Overload with more options
@Composable
fun RobohashImage(
robot: String,
modifier: Modifier = Modifier,
contentDescription: String? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
colorFilter: ColorFilter? = null,
loadRobohash: Boolean = true
)
```
- Use for: User avatars, deterministic graphics
- Pattern: Uses CachedRobohash.get(), isLightTheme() detection
- Fallback: Icons.Default.Face
- Works on: Android, Desktop (pure ImageVector)
**Theme Detection Helper**:
```kotlin
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
return (background.red + background.green + background.blue) / 3 > 0.5f
}
```
## Feed Components (`ui/feed/`)
### FeedHeader
**FeedHeader** - Screen header with title and relay status
```kotlin
@Composable
fun FeedHeader(
title: String,
connectedRelayCount: Int,
onRefresh: () -> Unit,
modifier: Modifier = Modifier
)
```
- Pattern: Row with SpaceBetween, title + RelayStatusIndicator
- Works on: Android, Desktop
**RelayStatusIndicator** - Compact relay connection indicator
```kotlin
@Composable
fun RelayStatusIndicator(
connectedCount: Int,
onRefresh: () -> Unit,
modifier: Modifier = Modifier
)
```
- Pattern: Status icon + count text + refresh button
- Colors: RelayStatusColors.{Disconnected, Connecting, Connected}
- Visual cues: Check icon (connected), Close icon (disconnected)
## Screens (`ui/screens/`)
### Placeholder Pattern
**PlaceholderScreen** - Generic placeholder
```kotlin
@Composable
fun PlaceholderScreen(
title: String,
description: String,
modifier: Modifier = Modifier
)
```
- Pattern: Column with title (headlineMedium) + description
- Use for: Unimplemented screens, coming soon features
**Specific Placeholders**:
- `SearchPlaceholder()` - Search screen
- `MessagesPlaceholder()` - DMs screen
- `NotificationsPlaceholder()` - Notifications screen
Pattern: Specific implementations wrap PlaceholderScreen with preset text.
## Custom Icons (`robohash/parts/`)
### ImageVector Builder Pattern
Amethyst uses a custom DSL for building ImageVector assets:
```kotlin
@Composable
fun Face0C3po() {
Image(
painter = rememberVectorPainter(
roboBuilder {
face0C3po(SolidColor(Color.Blue), this)
}
),
contentDescription = ""
)
}
fun face0C3po(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
// ...
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
// ... path commands
}
```
**roboBuilder** - Custom ImageVector.Builder DSL
- Located in: `commons/robohash/`
- Pattern: Builder-based, composable paths
- Parts: Face, Eyes, Mouth, Body, Accessory (0-9 variants each)
- Colors: Dynamic (fgColor parameter) + Black constants
### CachedRobohash
```kotlin
CachedRobohash.get(seed: String, isLight: Boolean): ImageVector
```
- Deterministic: Same seed → same avatar
- Theme-aware: Different colors for light/dark
- Cached: Performance optimization
- Pure ImageVector: Works on all platforms
## Sharing Guidelines
### Always Share
- State visualization (Loading, Empty, Error)
- Action buttons with consistent styling
- Generic placeholders
- Custom ImageVector icons
- Material3 themed components
- Theme utilities (isLightTheme)
### Platform-Specific (Delegate to Experts)
- Navigation structure (android-expert, desktop-expert)
- Screen layouts and scaffolds
- Platform system integrations
- Gesture handling specifics
### Decision Framework
1. **Can it use Material3 primitives?** → Share
2. **Does it need platform system APIs?** → Platform-specific
3. **Is it a visual component without navigation?** → Share
4. **Does it require platform UX patterns?** → Ask platform expert
## Material3 Usage
All shared composables use Material3:
- `MaterialTheme.colorScheme.*` for colors
- `MaterialTheme.typography.*` for text styles
- `OutlinedButton`, `Button`, `IconButton` for actions
- `CircularProgressIndicator` for loading
- `Icon`, `Image` for visuals
This ensures consistent theming across Android and Desktop.
@@ -0,0 +1,334 @@
# Compose State Management Patterns
Visual guide to state management in Compose Multiplatform. For Kotlin-specific patterns (StateFlow, sealed classes), see `kotlin-expert` skill.
## Core State Functions
### remember
Cache values across recompositions:
```kotlin
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
```
**When to use**: Simple UI state (toggles, counters, text input)
**Visual pattern**: Button press → state changes → UI updates
### derivedStateOf
Compute state from other state, recompose only when result changes:
```kotlin
@Composable
fun ScrollToTopButton(listState: LazyListState) {
// Only recomposes when showButton value changes (not every scroll pixel)
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
```
**When to use**: Input state changes frequently, but derived result changes rarely
**Visual pattern**: Scroll position (0, 1, 2...) → boolean (show/hide) → FAB visibility
**Performance**: Prevents recomposition on every scroll event
### produceState
Convert non-Compose state into Compose state:
```kotlin
@Composable
fun LoadUserProfile(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = repository.fetchUser(userId)
}
}
@Composable
fun ProfileScreen(userId: String) {
val user by LoadUserProfile(userId)
when (user) {
null -> LoadingState("Loading profile...")
else -> ProfileCard(user!!)
}
}
```
**When to use**: Convert Flow, LiveData, callbacks into Compose state
**Visual pattern**: Async operation → state updates → UI reflects changes
**Lifecycle**: Coroutine cancelled when composable leaves composition
## State Hoisting Pattern
Move state up to make composables reusable and testable:
### Before (Stateful)
```kotlin
@Composable
fun SearchBar() {
var query by remember { mutableStateOf("") }
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("Search...") }
)
}
```
❌ Hard to test, can't control state externally
### After (Stateless)
```kotlin
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
placeholder = { Text("Search...") },
modifier = modifier
)
}
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
Column {
SearchBar(query = query, onQueryChange = { query = it })
SearchResults(query = query)
}
}
```
✅ Reusable, testable, state controlled by parent
**Hoisting principle**: State goes up, events go down
- State: `query: String` (read-only)
- Events: `onQueryChange: (String) -> Unit` (write-only)
## Amethyst State Patterns
### Theme-Aware State
```kotlin
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
return (background.red + background.green + background.blue) / 3 > 0.5f
}
@Composable
fun ThemedContent() {
val isDark = !isLightTheme()
// Adjust visuals based on theme
val iconTint = if (isDark) Color.White else Color.Black
}
```
**Pattern**: Derive state from MaterialTheme
**Visual**: Component adapts to light/dark theme automatically
### Relay Status State
```kotlin
@Composable
fun RelayStatusIndicator(
connectedCount: Int,
onRefresh: () -> Unit,
modifier: Modifier = Modifier
) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
tint = statusColor
)
}
```
**Pattern**: Visual state derived from domain state
**Visual mapping**:
- 0 relays → Red + X icon
- 1-2 relays → Yellow + Check icon
- 3+ relays → Green + Check icon
### Loading/Empty/Error States
```kotlin
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> LoadingState("Loading feed...")
is UiState.Empty -> FeedEmptyState(onRefresh = { viewModel.refresh() })
is UiState.Error -> FeedErrorState(
errorMessage = uiState.message,
onRetry = { viewModel.retry() }
)
is UiState.Success -> LazyColumn {
items(uiState.items) { FeedItem(it) }
}
}
}
```
**Pattern**: Sealed class → visual state component
**Components**:
- `LoadingState` - Progress indicator
- `EmptyState` - Empty message + refresh
- `ErrorState` - Error message + retry
- Success - Actual content
## Common Patterns
### Toggle State
```kotlin
var isExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { isExpanded = !isExpanded }) {
Icon(
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Collapse" else "Expand"
)
}
if (isExpanded) {
Text("Expanded content...")
}
```
### List State with Actions
```kotlin
var items by remember { mutableStateOf(listOf("Item 1", "Item 2")) }
Column {
AddButton(onClick = {
items = items + "Item ${items.size + 1}"
})
items.forEachIndexed { index, item ->
Row {
Text(item)
RemoveButton(onClick = {
items = items.filterIndexed { i, _ -> i != index }
})
}
}
}
```
### TextField State
```kotlin
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") }
)
```
## Performance Patterns
### Avoid Unnecessary Recomposition
```kotlin
// ❌ Bad: Recomposes on every scroll position change
@Composable
fun BadScrollButton(scrollState: ScrollState) {
if (scrollState.value > 100) { // scrollState.value changes constantly
Button(onClick = { /* ... */ }) { Text("Scroll to Top") }
}
}
// ✅ Good: Only recomposes when visibility changes
@Composable
fun GoodScrollButton(scrollState: ScrollState) {
val showButton by remember {
derivedStateOf { scrollState.value > 100 }
}
if (showButton) {
Button(onClick = { /* ... */ }) { Text("Scroll to Top") }
}
}
```
### Stable Parameters
Use `@Immutable` data classes (see `kotlin-expert`) to prevent recomposition:
```kotlin
@Immutable
data class UserProfile(val name: String, val avatar: String)
@Composable
fun ProfileCard(profile: UserProfile) {
// Only recomposes when profile instance changes
Row {
RobohashImage(robot = profile.avatar)
Text(profile.name)
}
}
```
## Integration with Kotlin State
For ViewModel state, Flow, StateFlow → See `kotlin-expert` skill
Common integration pattern:
```kotlin
// ViewModel (Kotlin state)
class FeedViewModel {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// Composable (Compose state)
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val uiState by viewModel.uiState.collectAsState()
// Use uiState to render UI
}
```
## Quick Reference
| Function | Use Case | Recomposes When |
|----------|----------|----------------|
| `remember { mutableStateOf() }` | Local UI state | State value changes |
| `derivedStateOf { }` | Computed state | Derived result changes |
| `produceState { }` | Async/Flow → State | Async operation updates value |
| `collectAsState()` | Flow → State | Flow emits new value |
| State hoisting | Reusable components | Parent passes new state |
## Sources
State management patterns based on:
- [State and Jetpack Compose - Android Developers](https://developer.android.com/develop/ui/compose/state)
- [When should I use derivedStateOf?](https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b)
- [Advanced State and Side Effects](https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects)
- AmethystMultiplatform codebase patterns (2025)
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Find all @Composable functions in the codebase
set -e
# Default to current directory if no path provided
SEARCH_PATH="${1:-.}"
echo "Searching for @Composable functions in: $SEARCH_PATH"
echo "================================================"
echo ""
# Find all @Composable functions with file paths and line numbers
grep -r -n "@Composable" "$SEARCH_PATH" \
--include="*.kt" \
--exclude-dir=build \
--exclude-dir=.gradle \
| while IFS=: read -r file line content; do
# Extract function name if possible
if [[ $content =~ fun[[:space:]]+([a-zA-Z0-9_]+) ]]; then
func_name="${BASH_REMATCH[1]}"
echo "$file:$line - $func_name"
else
echo "$file:$line"
fi
done
echo ""
echo "Total @Composable functions found:"
grep -r "@Composable" "$SEARCH_PATH" \
--include="*.kt" \
--exclude-dir=build \
--exclude-dir=.gradle \
| wc -l
+548
View File
@@ -0,0 +1,548 @@
---
name: gradle-expert
description: Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencies and source sets, (5) Desktop packaging (DMG/MSI/DEB), (6) Build performance optimization, (7) Proguard/R8 configuration, (8) Common KMP + Android Gradle issues (Compose conflicts, secp256k1 JNI variants, source set problems).
---
# Gradle Expert
Build system expertise for AmethystMultiplatform's 4-module KMP architecture. Focus: practical troubleshooting, dependency resolution, and project-specific optimizations.
## Build Architecture Mental Model
Think of this project as **4 layers**:
```
┌─────────────┬─────────────┐
│ :amethyst │ :desktopApp │ ← Platform apps (navigation, layouts)
│ (Android) │ (JVM) │
└──────┬──────┴──────┬──────┘
│ │
└──────┬──────┘
┌─────────────┐
│ :commons │ ← Shared UI (KMP with jvmAndroid)
│ (KMP UI) │
└──────┬──────┘
┌─────────────┐
│ :quartz │ ← Core library (KMP: Android/JVM/iOS)
│(KMP Library)│
└─────────────┘
```
**Key insight:** Dependencies flow DOWN. Lower modules never depend on upper modules. This enables code sharing without circular dependencies.
**The jvmAndroid pattern:** Unique to this project. A custom source set between commonMain and {androidMain, jvmMain} for JVM-specific code shared by Android and Desktop. Not standard KMP, but critical for this architecture.
## Version Catalog Philosophy
All dependencies centralized in `gradle/libs.versions.toml`. Think "single source of truth."
**Pattern:**
```toml
[versions]
kotlin = "2.3.0"
[libraries]
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
```
**Usage:**
```kotlin
dependencies {
implementation(libs.okhttp) // Type-safe, IDE-autocompleted
}
```
**Critical alignments:**
- **Kotlin ecosystem:** All Kotlin plugins MUST share same version
- **Compose ecosystem:** Compose Multiplatform version → Kotlin version (check compatibility matrix)
- **secp256k1 variants:** All three variants (common, jni-android, jni-jvm) MUST share same version
See [references/version-catalog-guide.md](references/version-catalog-guide.md) for comprehensive patterns.
## Common Build Tasks
### Quick Reference
```bash
# Full builds
./gradlew build # All modules
./gradlew clean build # Clean build
# Desktop
./gradlew :desktopApp:run # Run desktop app
./gradlew :desktopApp:packageDmg # macOS package
# Module-specific
./gradlew :quartz:build # KMP library only
./gradlew :commons:build # Shared UI only
# Analysis
./gradlew dependencies # Dependency tree
./gradlew build --scan # Online diagnostics
```
See [references/build-commands.md](references/build-commands.md) for comprehensive command reference.
## Module Structure & Dependencies
### Dependency Flow
**Desktop build chain:**
```
:desktopApp → :commons (jvmMain) → :quartz (jvmMain → jvmAndroid → commonMain)
```
**Android build chain:**
```
:amethyst → :commons (androidMain) → :quartz (androidMain → jvmAndroid → commonMain)
```
**Key source set pattern (quartz & commons):**
```
commonMain # Truly cross-platform code
├─ jvmAndroid # JVM-specific, shared by Android + Desktop
│ ├─ androidMain
│ └─ jvmMain
└─ iosMain # iOS-specific (quartz only)
```
**Dependency config types:**
- Use `api` when types appear in module's public API or expect/actual declarations
- Use `implementation` for internal implementation details
- Example: quartz exposes secp256k1 (`api`), but hides okhttp (`implementation`)
See [references/dependency-graph.md](references/dependency-graph.md) for module visualization and transitive dependency flow.
## Critical Dependency Patterns
### 1. secp256k1 (Crypto Library)
**The problem:** KMP library with platform-specific JNI bindings. Wrong variant = runtime crash.
**Pattern:**
```kotlin
// commonMain - API only
api(libs.secp256k1.kmp.common)
// androidMain - Android JNI
api(libs.secp256k1.kmp.jni.android)
// jvmMain - Desktop JVM JNI
implementation(libs.secp256k1.kmp.jni.jvm)
```
**Why api in androidMain?** Types leak to consumers (:amethyst).
**Common error:** Desktop using jni-android variant → `UnsatisfiedLinkError: no secp256k1jni in java.library.path`
**Fix:** Check source set dependencies. jvmMain must use jni-jvm, never jni-android.
### 2. JNA (for LibSodium Encryption)
**The problem:** Android needs AAR packaging, JVM needs JAR. Same library, different artifact types.
**Pattern:**
```kotlin
// androidMain
implementation("com.goterl:lazysodium-android:5.2.0@aar") // @aar explicit
implementation("net.java.dev.jna:jna:5.18.1@aar")
// jvmMain
implementation(libs.lazysodium.java) // JAR implicit
implementation(libs.jna)
```
**Critical:** Never put JNA in jvmAndroid or commonMain. Platform-specific packaging only.
### 3. Compose Versions
**The problem:** Two Compose ecosystems (Multiplatform + AndroidX) must align, or duplicate classes.
**Current project config:**
```toml
composeMultiplatform = "1.9.3" # Plugin + runtime
composeBom = "2025.12.01" # AndroidX Compose BOM
kotlin = "2.3.0"
```
**Rule:** Compose Multiplatform version must be compatible with Kotlin version. Check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html
**In KMP modules (quartz, commons):**
```kotlin
// ✅ Use Compose Multiplatform
implementation(compose.ui)
implementation(compose.material3)
// ❌ DON'T use AndroidX BOM in KMP modules
// implementation(libs.androidx.compose.bom)
```
**In Android-only modules (amethyst):**
```kotlin
// Can use AndroidX BOM
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
```
## Desktop Packaging Basics
**TargetFormat options:**
```kotlin
// In desktopApp/build.gradle.kts
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Amethyst"
packageVersion = "1.0.0"
macOS {
bundleID = "com.vitorpamplona.amethyst.desktop"
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
}
}
```
**Package tasks:**
```bash
./gradlew :desktopApp:packageDmg # macOS
./gradlew :desktopApp:packageMsi # Windows
./gradlew :desktopApp:packageDeb # Linux
```
**Output locations:**
- macOS: `desktopApp/build/compose/binaries/main/dmg/`
- Windows: `desktopApp/build/compose/binaries/main/msi/`
- Linux: `desktopApp/build/compose/binaries/main/deb/`
**Icon requirements:**
- macOS: `.icns` (multi-resolution: 512, 256, 128, 32)
- Windows: `.ico` (256, 128, 64, 32, 16)
- Linux: `.png` (512x512)
**Common issues:**
- Main class not found → Verify `mainClass = "...MainKt"` (Kotlin adds `Kt` suffix)
- Native libs missing → Ensure secp256k1-kmp-jni-jvm in dependencies
- Icon not found → Check file exists at path, use absolute path if needed
## Build Performance Optimization
**Add to `gradle.properties`:**
```properties
# Daemon (faster subsequent builds)
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
# Parallel execution (multi-module speedup)
org.gradle.parallel=true
org.gradle.workers.max=8
# Caching (incremental builds)
org.gradle.caching=true
org.gradle.configuration-cache=true
# Kotlin daemon
kotlin.incremental=true
kotlin.daemon.jvmargs=-Xmx2g
```
**Impact:** Typically 30-50% faster builds after first run.
**Measure impact:**
```bash
./gradlew clean build --profile
# Report: build/reports/profile/profile-<timestamp>.html
```
**When to clean build:**
- After changing version catalog
- After adding/removing source sets
- When seeing unexplained errors
**When NOT to clean:**
- Regular development iteration
- Small code changes
- Incremental compilation works fine
Use script: `scripts/analyze-build-time.sh` for automated profiling.
## Troubleshooting: Practical Patterns
### Pattern 1: Version Conflict
**Symptom:** `Duplicate class` or `NoSuchMethodError`
**Diagnosis:**
```bash
./gradlew dependencyInsight --dependency <library-name>
```
**Fix options:**
1. Align versions in libs.versions.toml (preferred)
2. Force resolution:
```kotlin
configurations.all {
resolutionStrategy {
force(libs.okhttp.get().toString())
}
}
```
### Pattern 2: Source Set Issues
**Symptom:** `Unresolved reference` to JVM library in shared code
**Diagnosis:** Check source set hierarchy. JVM-only libs (jackson, okhttp) can't be in commonMain.
**Fix:** Move to jvmAndroid or platform-specific source set.
```kotlin
// ❌ Wrong
commonMain {
dependencies {
implementation(libs.jackson.module.kotlin) // JVM-only!
}
}
// ✅ Correct
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
api(libs.jackson.module.kotlin) // JVM code, shared by Android + Desktop
}
}
```
### Pattern 3: Proguard Stripping Native Libs
**Symptom:** `NoClassDefFoundError` for secp256k1, JNA, or LibSodium in release builds
**Fix:** Update proguard rules in `quartz/proguard-rules.pro`:
```proguard
# Native libraries
-keep class fr.acinq.secp256k1.** { *; }
-keep class com.goterl.lazysodium.** { *; }
-keep class com.sun.jna.** { *; }
# Jackson (reflection-based)
-keep class com.vitorpamplona.quartz.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
```
### Pattern 4: Compose Compiler Mismatch
**Symptom:** `IllegalStateException: Version mismatch: runtime 1.10.0 but compiler 1.9.0`
**Fix:** Update Compose Multiplatform version in libs.versions.toml to match Kotlin version compatibility.
Check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html
### Pattern 5: Wrong JVM Target
**Symptom:** `Unsupported class file major version 65`
**Fix:** Ensure Java 21 everywhere:
```bash
# Check current Java
java -version # Should show 21
# Set JAVA_HOME
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
# Stop Gradle daemon to pick up new Java
./gradlew --stop
```
Verify all build files use JVM 21:
```kotlin
kotlin {
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
}
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
```
## Comprehensive Error Guide
For detailed troubleshooting of specific errors, see [references/common-errors.md](references/common-errors.md). Covers:
- Compose version conflicts
- secp256k1 JNI errors
- Source set dependency issues
- Proguard/R8 problems
- Desktop packaging errors
- Kotlin compilation errors
- Dependency resolution failures
- JVM/JDK version issues
Each error includes: symptom, cause, solution, verification steps.
## Quick Diagnostic Commands
```bash
# Check dependencies for specific module
./gradlew :quartz:dependencies
# Find specific library in dependency tree
./gradlew dependencyInsight --dependency okhttp
# Build with detailed logging
./gradlew build --info
# Generate interactive build scan (best diagnostics)
./gradlew build --scan
# Profile build performance
./gradlew clean build --profile
# Stop all Gradle daemons (fresh start)
./gradlew --stop
# Check Gradle version
./gradlew --version
```
## Scripts & References
### Diagnostic Scripts
- `scripts/analyze-build-time.sh` - Profile build performance, generate optimization report
- `scripts/fix-dependency-conflicts.sh` - Diagnose common dependency conflicts, suggest fixes
### Reference Docs
- `references/build-commands.md` - Comprehensive command reference for all tasks
- `references/dependency-graph.md` - Module dependencies, source set hierarchy, transitive deps
- `references/version-catalog-guide.md` - Version catalog patterns, usage, best practices
- `references/common-errors.md` - Troubleshooting guide for frequent build issues
## Workflow Examples
### Example 1: Adding New Dependency
**Task:** Add kotlinx.datetime to quartz
**Steps:**
1. **Update version catalog** (gradle/libs.versions.toml):
```toml
[versions]
kotlinxDatetime = "0.6.0"
[libraries]
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
```
2. **Add to build file** (quartz/build.gradle.kts):
```kotlin
sourceSets {
commonMain {
dependencies {
implementation(libs.kotlinx.datetime) // KMP library, goes in commonMain
}
}
}
```
3. **Sync & verify:**
```bash
./gradlew :quartz:dependencies | grep datetime
```
### Example 2: Fixing secp256k1 Error on Desktop
**Error:** `UnsatisfiedLinkError: no secp256k1jni in java.library.path` when running desktop app
**Diagnosis:**
```bash
./gradlew :desktopApp:dependencies --configuration runtimeClasspath | grep secp256k1
# Shows: secp256k1-kmp-jni-android ← WRONG!
```
**Fix:**
```kotlin
// In quartz/build.gradle.kts
jvmMain {
dependencies {
// Change from:
// implementation(libs.secp256k1.kmp.jni.android) ❌
// To:
implementation(libs.secp256k1.kmp.jni.jvm) // ✅
}
}
```
**Verify:**
```bash
./gradlew :desktopApp:dependencies --configuration runtimeClasspath | grep secp256k1
# Now shows: secp256k1-kmp-jni-jvm ✅
./gradlew :desktopApp:run # Should work
```
### Example 3: Optimizing Build Time
**Current:** Clean build takes 5 minutes
**Steps:**
1. **Baseline measurement:**
```bash
./gradlew clean build --profile
# Check: build/reports/profile/profile-*.html
```
2. **Add optimizations** to gradle.properties:
```properties
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx4g
kotlin.incremental=true
```
3. **Re-measure:**
```bash
./gradlew clean build --profile
```
**Expected improvement:** 30-50% faster on subsequent builds (incremental builds much faster).
## Delegation Patterns
**When to delegate to other skills:**
- **Source set architecture** (jvmAndroid pattern, expect/actual) → Use `kotlin-multiplatform` skill
- **Compose UI issues** (composables, state management) → Use `compose-expert` skill (when available)
- **Kotlin language issues** (Flow, sealed classes, DSLs) → Use `kotlin-expert` skill
- **Desktop-specific features** (Window management, MenuBar, tray) → Use `desktop-expert` skill
**This skill handles:** Build system, dependencies, versioning, module structure, packaging, performance.
## Core Principles for This Build System
1. **Centralize versions:** Never hardcode versions in build.gradle.kts. Always use libs.versions.toml.
2. **Respect source set hierarchy:** Dependencies flow downward. jvmAndroid depends on commonMain, never the reverse.
3. **Platform-specific variants matter:** secp256k1, JNA must use correct variant per platform. Check when errors occur.
4. **Clean builds are expensive:** Use incremental compilation. Only clean when truly needed (source set changes, version updates).
5. **Compose alignment is critical:** Compose Multiplatform version must match Kotlin version. Check compatibility matrix.
6. **Proguard for native libs:** All JNI libraries need explicit `-keep` rules in release builds.
7. **Java 21 everywhere:** All modules, all targets, consistent JVM version.
@@ -0,0 +1,214 @@
# Build Commands Reference
## Table of Contents
- [Core Build Tasks](#core-build-tasks)
- [Module-Specific Builds](#module-specific-builds)
- [Desktop Tasks](#desktop-tasks)
- [Android Tasks](#android-tasks)
- [Testing](#testing)
- [Analysis & Diagnostics](#analysis--diagnostics)
- [Performance Optimization](#performance-optimization)
## Core Build Tasks
### Full Project Build
```bash
./gradlew build # Build all modules
./gradlew clean build # Clean build
./gradlew assemble # Build without tests
```
### Incremental Builds
```bash
./gradlew :quartz:build # Build only quartz module
./gradlew :commons:build # Build only commons module
./gradlew :desktopApp:build # Build only desktop app
```
## Module-Specific Builds
### Quartz (KMP Library)
```bash
./gradlew :quartz:build # All targets
./gradlew :quartz:compileKotlinJvm # JVM target only
./gradlew :quartz:compileDebugKotlinAndroid # Android target only
./gradlew :quartz:linkDebugFrameworkIosArm64 # iOS framework
./gradlew :quartz:publishToMavenLocal # Publish locally
```
### Commons (Shared UI)
```bash
./gradlew :commons:build # All targets
./gradlew :commons:compileKotlinJvm # Desktop target
./gradlew :commons:compileDebugKotlinAndroid # Android target
```
## Desktop Tasks
### Run Desktop App
```bash
./gradlew :desktopApp:run # Run desktop app
./gradlew :desktopApp:runDistributable # Run packaged version
```
### Package Desktop App
```bash
./gradlew :desktopApp:createDistributable # Create runnable package
./gradlew :desktopApp:packageDmg # macOS DMG
./gradlew :desktopApp:packageMsi # Windows MSI
./gradlew :desktopApp:packageDeb # Linux DEB
```
### Distribution Location
- macOS: `desktopApp/build/compose/binaries/main/dmg/`
- Windows: `desktopApp/build/compose/binaries/main/msi/`
- Linux: `desktopApp/build/compose/binaries/main/deb/`
## Android Tasks
### Compile & Assemble
```bash
./gradlew :amethyst:assembleDebug # Debug APK
./gradlew :amethyst:assembleRelease # Release APK
./gradlew :amethyst:bundleRelease # Release AAB
```
### Install & Run
```bash
./gradlew :amethyst:installDebug # Install debug on device
adb shell am start -n com.vitorpamplona.amethyst/.MainActivity
```
### Proguard/R8
```bash
./gradlew :quartz:minifyReleaseWithR8 # Test R8 minification
```
## Testing
### Unit Tests
```bash
./gradlew test # All unit tests
./gradlew :quartz:jvmTest # JVM unit tests
./gradlew :quartz:testDebugUnitTest # Android unit tests
./gradlew :commons:test # Commons tests
```
### Android Instrumented Tests
```bash
./gradlew :quartz:connectedAndroidTest # Requires device/emulator
```
### Test Reports
```bash
# Reports location: <module>/build/reports/tests/
open quartz/build/reports/tests/jvmTest/index.html
```
## Analysis & Diagnostics
### Dependency Analysis
```bash
./gradlew dependencies # All dependencies
./gradlew :quartz:dependencies # Quartz dependencies
./gradlew dependencyInsight --dependency okhttp # Specific dependency
```
### Build Scan
```bash
./gradlew build --scan # Upload to scans.gradle.com
```
### Performance Profiling
```bash
./gradlew build --profile # Generate profile report
# Report: build/reports/profile/profile-<timestamp>.html
```
### Task Dependencies
```bash
./gradlew :desktopApp:run --dry-run # Show task graph
./gradlew :desktopApp:dependencies --scan # Visualize dependencies
```
## Performance Optimization
### Configuration Cache
```bash
./gradlew build --configuration-cache # Enable config cache
./gradlew build --configuration-cache-problems=warn
```
### Build Cache
```bash
./gradlew build --build-cache # Enable build cache
./gradlew cleanBuildCache # Clear build cache
```
### Parallel Execution
```bash
./gradlew build --parallel --max-workers=8 # Parallel with 8 workers
```
### Daemon Management
```bash
./gradlew --stop # Stop Gradle daemon
./gradlew --status # Daemon status
```
### Incremental Compilation
```bash
# Already enabled by default in Kotlin, but can verify:
./gradlew :quartz:compileKotlinJvm --info | grep "Incremental"
```
## gradle.properties Optimizations
Add to `gradle.properties` for faster builds:
```properties
# Daemon
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
# Parallel
org.gradle.parallel=true
org.gradle.workers.max=8
# Caching
org.gradle.caching=true
org.gradle.configuration-cache=true
# Kotlin
kotlin.incremental=true
kotlin.daemon.jvmargs=-Xmx2g
```
## Common Workflows
### Full Desktop Build & Run
```bash
./gradlew :desktopApp:clean :desktopApp:run
```
### Quick Desktop Iteration
```bash
# No clean - incremental compilation
./gradlew :desktopApp:run
```
### Android Release Build
```bash
./gradlew :amethyst:clean :amethyst:bundleRelease
```
### Test All KMP Targets
```bash
./gradlew :quartz:test :quartz:testDebugUnitTest
```
### Publish Quartz Locally for Testing
```bash
./gradlew :quartz:publishToMavenLocal
# Then update version in consumer project to test
```
@@ -0,0 +1,643 @@
# Common Build Errors & Solutions
## Table of Contents
- [Compose Version Conflicts](#compose-version-conflicts)
- [secp256k1 JNI Errors](#secp256k1-jni-errors)
- [Source Set Dependency Issues](#source-set-dependency-issues)
- [Proguard/R8 Issues](#proguardr8-issues)
- [Desktop Packaging Errors](#desktop-packaging-errors)
- [Kotlin Compilation Errors](#kotlin-compilation-errors)
- [Dependency Resolution Failures](#dependency-resolution-failures)
- [JVM/JDK Version Issues](#jvmjdk-version-issues)
---
## Compose Version Conflicts
### Error 1: Compose Runtime Mismatch
```
java.lang.IllegalStateException: Version mismatch: Compose runtime is 1.10.0 but compiler is 1.9.0
```
**Cause:** Compose Compiler plugin version doesn't match Compose Runtime
**Solution:**
```kotlin
// In gradle/libs.versions.toml
composeMultiplatform = "1.9.3" // Must align with Kotlin version
kotlin = "2.3.0"
// Check compatibility matrix:
// https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html
```
**Verification:**
```bash
./gradlew :commons:dependencies | grep compose
```
### Error 2: AndroidX Compose BOM Conflict
```
Duplicate class androidx.compose.ui.platform.AndroidCompositionLocalMap found in modules...
```
**Cause:** Both Compose Multiplatform and AndroidX Compose BOM providing same classes
**Solution:**
```kotlin
// In commons/build.gradle.kts (KMP module)
// Use Compose Multiplatform, NOT AndroidX BOM
dependencies {
implementation(compose.ui) // ✅ Compose Multiplatform
implementation(compose.material3)
// Don't use in KMP modules:
// implementation(libs.androidx.compose.bom) // ❌ Android-only
}
// In amethyst/build.gradle.kts (Android-only module)
// Can use AndroidX BOM
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
implementation(libs.androidx.ui)
}
```
### Error 3: Material3 WindowSizeClass Not Found
```
Unresolved reference: WindowSizeClass
```
**Cause:** Using Android's WindowSizeClass in shared KMP code
**Solution:**
```kotlin
// Don't use in commonMain or jvmAndroid:
// import androidx.compose.material3.windowsizeclass.WindowSizeClass // ❌
// Use in androidMain only, or create expect/actual:
// commonMain
expect class WindowSizeClassAdapter
// androidMain
actual typealias WindowSizeClassAdapter = androidx.compose.material3.windowsizeclass.WindowSizeClass
// jvmMain (desktop)
actual class WindowSizeClassAdapter { /* Custom impl */ }
```
---
## secp256k1 JNI Errors
### Error 1: JNI Library Not Found (Desktop)
```
java.lang.UnsatisfiedLinkError: no secp256k1jni in java.library.path
```
**Cause:** Desktop using wrong secp256k1 variant (Android JNI instead of JVM JNI)
**Solution:**
```kotlin
// In quartz/build.gradle.kts
sourceSets {
jvmMain {
dependencies {
// ✅ Correct - JVM variant
implementation(libs.secp256k1.kmp.jni.jvm)
// ❌ Wrong - Android variant
// implementation(libs.secp256k1.kmp.jni.android)
}
}
}
```
**Verification:**
```bash
./gradlew :quartz:dependencies --configuration jvmRuntimeClasspath | grep secp256k1
# Should show: secp256k1-kmp-jni-jvm, NOT jni-android
```
### Error 2: Version Mismatch Between Variants
```
java.lang.NoSuchMethodError: fr.acinq.secp256k1.Secp256k1.sign
```
**Cause:** Common, Android, and JVM variants have different versions
**Solution:**
```toml
# In gradle/libs.versions.toml
# All three MUST use same version
secp256k1KmpJniAndroid = "0.22.0"
[libraries]
secp256k1-kmp-common = { ..., version.ref = "secp256k1KmpJniAndroid" }
secp256k1-kmp-jni-android = { ..., version.ref = "secp256k1KmpJniAndroid" }
secp256k1-kmp-jni-jvm = { ..., version.ref = "secp256k1KmpJniAndroid" }
```
### Error 3: Android JNI Not Loaded
```
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader couldn't find "libsecp256k1jni.so"
```
**Cause:** Proguard stripping JNI classes
**Solution:**
```proguard
# In quartz/proguard-rules.pro
-keep class fr.acinq.secp256k1.** { *; }
```
---
## Source Set Dependency Issues
### Error 1: jvmAndroid Defined After androidMain
```
Could not get unknown property 'jvmAndroid' for source set container
```
**Cause:** Source sets must be defined in dependency order
**Solution:**
```kotlin
// ✅ Correct order
sourceSets {
commonMain { }
// Define jvmAndroid BEFORE androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
}
androidMain {
dependsOn(jvmAndroid) // Now jvmAndroid exists
}
jvmMain {
dependsOn(jvmAndroid)
}
}
```
### Error 2: Dependency in Wrong Source Set
```
Unresolved reference: ObjectMapper (Jackson)
```
**Cause:** JVM-only library in commonMain
**Solution:**
```kotlin
sourceSets {
commonMain {
// ❌ Jackson is JVM-only, can't use here
// implementation(libs.jackson.module.kotlin)
}
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
// ✅ Jackson in jvmAndroid (shared JVM code)
api(libs.jackson.module.kotlin)
}
}
```
### Error 3: Platform-Specific Code in Shared Source Set
```
java.lang.NoClassDefFoundError: android.content.Context
```
**Cause:** Android-specific API in jvmAndroid or commonMain
**Solution:**
```kotlin
// Use expect/actual pattern
// commonMain
expect class PlatformContext
// androidMain
actual typealias PlatformContext = android.content.Context
// jvmMain
actual class PlatformContext {
// Custom desktop implementation
}
```
---
## Proguard/R8 Issues
### Error 1: Native Library Classes Stripped
```
java.lang.NoClassDefFoundError: com.goterl.lazysodium.Sodium
```
**Cause:** R8/Proguard removing JNA/LibSodium classes
**Solution:**
```proguard
# In quartz/proguard-rules.pro
-keep class com.goterl.lazysodium.** { *; }
-keep class com.sun.jna.** { *; }
-keep class fr.acinq.secp256k1.** { *; }
```
### Error 2: Reflection-Based Libraries Broken
```
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of ...
```
**Cause:** Jackson uses reflection, R8 strips class metadata
**Solution:**
```proguard
# Preserve reflection metadata
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
# Keep all Quartz event classes
-keep class com.vitorpamplona.quartz.** { *; }
```
### Error 3: Enum Values Missing
```
java.lang.IllegalArgumentException: No enum constant ...
```
**Cause:** R8 obfuscating enum names
**Solution:**
```proguard
# Keep all enums
-keep enum ** { *; }
-keepnames class ** { *; }
```
---
## Desktop Packaging Errors
### Error 1: Icon Not Found
```
FAILURE: Build failed with an exception.
* What went wrong: Cannot find icon file: src/jvmMain/resources/icon.icns
```
**Cause:** Icon file missing or wrong path
**Solution:**
```kotlin
// In desktopApp/build.gradle.kts
nativeDistributions {
macOS {
// Ensure file exists at this path
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
}
// Check file exists:
// ls -la desktopApp/src/jvmMain/resources/
}
```
**Icon Requirements:**
- macOS: `.icns` (512x512, 256x256, 128x128, 32x32)
- Windows: `.ico` (256x256, 128x128, 64x64, 32x32, 16x16)
- Linux: `.png` (512x512 recommended)
### Error 2: Main Class Not Found
```
Error: Could not find or load main class com.vitorpamplona.amethyst.desktop.MainKt
```
**Cause:** Wrong mainClass path or Main.kt doesn't have main()
**Solution:**
```kotlin
// In desktopApp/build.gradle.kts
compose.desktop {
application {
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
// ^^^^
// Kotlin compiler adds "Kt" suffix
}
}
// In src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt
fun main() = application {
// ...
}
```
### Error 3: Native Library Missing in Package
```
java.lang.UnsatisfiedLinkError: no secp256k1jni in java.library.path
```
**Cause:** Native libraries not bundled in distribution
**Solution:**
```kotlin
// Native libs are automatically included via dependencies
// Verify secp256k1-kmp-jni-jvm is in dependencies:
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm) // ✅ Includes native libs
}
// Test packaged app:
./gradlew :desktopApp:createDistributable
# Run from: desktopApp/build/compose/binaries/main/app/
```
---
## Kotlin Compilation Errors
### Error 1: Expect/Actual Mismatch
```
'actual' declaration has no corresponding expected declaration
```
**Cause:** Signature mismatch or missing expect
**Solution:**
```kotlin
// commonMain - expect declaration
expect class CryptoProvider {
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray
}
// androidMain & jvmMain - actual must match EXACTLY
actual class CryptoProvider {
actual fun sign(message: ByteArray, privateKey: ByteArray): ByteArray {
// Implementation
}
}
// Common mistakes:
// - Different parameter names ❌
// - Different return types ❌
// - Missing 'actual' modifier ❌
```
### Error 2: Target JVM Version Mismatch
```
Compilation failed: module was compiled with an incompatible version of Kotlin
```
**Cause:** Different JVM targets across modules
**Solution:**
```kotlin
// Ensure ALL modules use same JVM target
// In quartz/build.gradle.kts
kotlin {
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21) // ✅ Java 21
}
}
}
// In android {} block
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
```
### Error 3: Compose Compiler Plugin Missing
```
This declaration needs opt-in. Please use @OptIn(ComposeApi::class) or @Composable
```
**Cause:** Compose compiler plugin not applied
**Solution:**
```kotlin
// In build.gradle.kts
plugins {
alias(libs.plugins.jetbrainsComposeCompiler) // ✅ Add this
alias(libs.plugins.composeMultiplatform)
}
```
---
## Dependency Resolution Failures
### Error 1: Repository Not Found
```
Could not find com.github.vitorpamplona.compose-richtext:richtext-ui:f92ef49c9d
```
**Cause:** Jitpack or custom Maven repository not configured
**Solution:**
```kotlin
// In settings.gradle
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = "https://jitpack.io" } // ✅ Add Jitpack
}
}
```
### Error 2: Gradle Version Too Old
```
Version catalogs are not supported in this version of Gradle
```
**Cause:** Gradle < 7.0
**Solution:**
```properties
# In gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
```
Then: `./gradlew wrapper --gradle-version=8.9`
### Error 3: Dependency Variant Not Found
```
No matching variant of fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.22.0 was found
```
**Cause:** Wrong dependency configuration for target
**Solution:**
```kotlin
// In androidMain (Android library module)
dependencies {
// For AAR packaging
implementation("net.java.dev.jna:jna:5.18.1@aar") // ✅ Specify @aar
// secp256k1 works without @aar (auto-detects)
api(libs.secp256k1.kmp.jni.android)
}
```
---
## JVM/JDK Version Issues
### Error 1: Unsupported Class File Version
```
Unsupported class file major version 65
```
**Cause:** Compiled with Java 21, running with older Java
**Solution:**
```bash
# Check Java version
java -version # Should show 21
# Set JAVA_HOME if needed
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
# Or in gradle.properties
org.gradle.java.home=/path/to/jdk-21
```
### Error 2: JVM Toolchain Not Found
```
No matching toolchain found for requested JvmVersion
```
**Cause:** Java 21 not installed or not detected
**Solution:**
```bash
# macOS (Homebrew)
brew install openjdk@21
# Ubuntu
sudo apt install openjdk-21-jdk
# Set JAVA_HOME
export JAVA_HOME=$(/usr/libexec/java_home -v 21) # macOS
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk # Linux
# Verify
./gradlew -version
```
### Error 3: Gradle Daemon Using Wrong Java
```
Daemon will be stopped at the end of the build because JVM version has changed
```
**Cause:** Daemon started with different Java version
**Solution:**
```bash
# Stop all daemons
./gradlew --stop
# Start with correct JAVA_HOME
export JAVA_HOME=/path/to/jdk-21
./gradlew build
# Or set in gradle.properties permanently
org.gradle.java.home=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
```
---
## General Troubleshooting Steps
### Step 1: Clean Build
```bash
./gradlew clean
./gradlew --stop # Stop daemon
./gradlew build
```
### Step 2: Check Dependencies
```bash
./gradlew :moduleName:dependencies
./gradlew dependencyInsight --dependency libraryName
```
### Step 3: Enable Debug Logging
```bash
./gradlew build --info # Info logging
./gradlew build --debug # Debug logging (verbose)
./gradlew build --stacktrace
```
### Step 4: Invalidate Caches
```bash
# Clear Gradle cache
rm -rf ~/.gradle/caches/
# Clear build outputs
./gradlew clean
# Clear Gradle wrapper cache
rm -rf ~/.gradle/wrapper/
```
### Step 5: Build Scan
```bash
./gradlew build --scan
# Opens interactive diagnostics in browser
```
## Quick Reference: Error Keywords → Solution
| Error Keyword | Likely Cause | Quick Fix |
|---------------|--------------|-----------|
| `UnsatisfiedLinkError` | Wrong JNI variant | Check secp256k1/JNA variants by platform |
| `IllegalStateException` (Compose) | Version mismatch | Align Compose Multiplatform + Kotlin versions |
| `NoClassDefFoundError` | Proguard stripping | Add `-keep` rule for class |
| `Unresolved reference` | Wrong source set | Move to appropriate source set (jvmAndroid) |
| `Duplicate class` | BOM conflict | Remove AndroidX BOM from KMP modules |
| `Version mismatch` | Plugin/runtime version mismatch | Update libs.versions.toml |
| `No matching variant` | Repository or packaging issue | Add repository or @aar suffix |
| `Could not find` (dependency) | Missing repository | Add maven/jitpack to repositories |
| `Unsupported class file` | Java version mismatch | Update JAVA_HOME to Java 21 |
---
## Getting Help
1. **Check Build Scan**: `./gradlew build --scan` for detailed diagnostics
2. **Gradle Forums**: https://discuss.gradle.org/
3. **Kotlin Slack**: #multiplatform channel
4. **Stack Overflow**: Tags `gradle`, `kotlin-multiplatform`, `compose-multiplatform`
@@ -0,0 +1,266 @@
# Module Dependency Graph
## Visual Hierarchy
```
┌─────────────────────────────────────────────────────────┐
│ Root Project │
│ (Amethyst) │
└─────────────────────────────────────────────────────────┘
┌────────────────┼────────────────┬────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
│ :amethyst │ │ :desktopApp │ │ :benchmark │ │:ammolite │
│ (Android) │ │ (JVM) │ │ (Android) │ │ (Support)│
└─────────────┘ └─────────────┘ └─────────────┘ └──────────┘
│ │ │
│ │ │
└────────────────┼────────────────┘
┌─────────────┐
│ :commons │
│ (KMP UI) │
│ │
│ jvmAndroid │
│ / \ │
│ jvm android│
└─────────────┘
┌─────────────┐
│ :quartz │
│(KMP Library)│
│ │
│ commonMain │
│ │ │
│ jvmAndroid │
│ / | \ │
│jvm and ios │
└─────────────┘
```
## Module Details
### :quartz (KMP Nostr Library)
**Type:** Kotlin Multiplatform Library
**Targets:** JVM, Android, iOS (iosX64, iosArm64, iosSimulatorArm64)
**Dependencies:**
- External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable
- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain
**Role:** Core Nostr protocol implementation, shared across all platforms
### :commons (Shared UI Components)
**Type:** Kotlin Multiplatform Library
**Targets:** JVM, Android
**Dependencies:**
- Module: `:quartz`
- External: Compose Multiplatform, Material3, kotlinx.collections.immutable
- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}
**Role:** Shared Compose UI components for Desktop and Android
### :desktopApp (Desktop Application)
**Type:** JVM Application
**Targets:** JVM (Desktop)
**Dependencies:**
- Modules: `:commons`, `:quartz`
- External: Compose Desktop, kotlinx.coroutines.swing
**Role:** Desktop-specific navigation, layouts, and entry point
### :amethyst (Android Application)
**Type:** Android Application
**Targets:** Android
**Dependencies:**
- Modules: `:commons`, `:quartz`, `:ammolite`
- External: Android SDK, AndroidX, Firebase, Tor
**Role:** Android-specific navigation, layouts, and entry point
### :benchmark (Android Benchmark)
**Type:** Android Library
**Targets:** Android
**Dependencies:**
- Modules: `:commons`, `:quartz`
- External: AndroidX Benchmark
**Role:** Performance benchmarking for Android builds
### :ammolite (Support Module)
**Type:** Android Library
**Targets:** Android
**Dependencies:** Android-specific utilities
**Role:** Android support utilities for amethyst
## Dependency Flow Patterns
### Desktop Build Chain
```
:desktopApp → :commons (jvmMain) → :quartz (jvmMain)
jvmAndroid
commonMain
```
### Android Build Chain
```
:amethyst → :commons (androidMain) → :quartz (androidMain)
↓ ↓
:ammolite jvmAndroid
commonMain
```
## Source Set Dependencies
### :quartz Source Sets
```
commonMain (base)
├─ jvmAndroid (shared JVM code)
│ ├─ androidMain (Android platform)
│ └─ jvmMain (Desktop platform)
└─ iosMain (iOS platform)
├─ iosX64Main
├─ iosArm64Main
└─ iosSimulatorArm64Main
```
**Key Dependencies per Source Set:**
- **commonMain**: secp256k1-kmp, kotlinx.coroutines, collection, immutable collections
- **jvmAndroid**: jackson, okhttp, url-detector, rfc3986
- **androidMain**: secp256k1-kmp-jni-android, lazysodium-android, jna (aar)
- **jvmMain**: secp256k1-kmp-jni-jvm, lazysodium-java, jna (jar)
### :commons Source Sets
```
commonMain (base UI)
└─ jvmAndroid (shared JVM UI)
├─ androidMain (Android UI utilities)
└─ jvmMain (Desktop UI utilities)
```
**Key Dependencies per Source Set:**
- **commonMain**: Compose Multiplatform, Material3, :quartz
- **jvmAndroid**: url-detector
- **androidMain**: AndroidX Compose tooling
- **jvmMain**: Compose Desktop
## Critical Dependency Patterns
### 1. secp256k1 Variants
```kotlin
// commonMain - API only
api(libs.secp256k1.kmp.common)
// androidMain - JNI Android
api(libs.secp256k1.kmp.jni.android)
// jvmMain - JNI JVM
implementation(libs.secp256k1.kmp.jni.jvm)
```
**Why:** Different JNI bindings for Android vs Desktop JVM
### 2. JNA Variants (for LibSodium)
```kotlin
// androidMain
implementation("com.goterl:lazysodium-android:5.2.0@aar")
implementation("net.java.dev.jna:jna:5.18.1@aar")
// jvmMain
implementation(libs.lazysodium.java)
implementation(libs.jna) // JAR variant
```
**Why:** Android needs AAR packaging, JVM needs JAR
### 3. Compose Alignment
```kotlin
// commons/build.gradle.kts
implementation(compose.ui) // Compose Multiplatform BOM
implementation(compose.material3)
// Version catalog alignment
composeMultiplatform = "1.9.3"
composeBom = "2025.12.01" // AndroidX Compose
```
**Why:** Two Compose ecosystems (Multiplatform + AndroidX) must align
## Dependency Configuration Types
### API vs Implementation
**Use `api` when:**
- Dependency types appear in module's public API
- Used in expect/actual declarations visible to consumers
- Example: `secp256k1-kmp-common` in quartz (public types)
**Use `implementation` when:**
- Internal implementation detail
- Not exposed to module consumers
- Example: `okhttp` in quartz (internal network client)
### Example from quartz
```kotlin
// Public API - exposed to consumers
api(libs.secp256k1.kmp.common)
api(libs.jackson.module.kotlin) // Event serialization public
// Internal implementation
implementation(libs.okhttp)
implementation(libs.kotlinx.coroutines.core)
```
## Transitive Dependency Impact
### When :desktopApp depends on :commons
- Gets `:quartz` transitively (via :commons)
- Gets `secp256k1-kmp-jvm` transitively (via :quartz jvmMain)
- Does NOT get Android-specific dependencies (scoped to androidMain)
### When :amethyst depends on :commons
- Gets `:quartz` transitively (via :commons)
- Gets `secp256k1-kmp-jni-android` transitively (via :quartz androidMain)
- Does NOT get JVM/Desktop-specific dependencies (scoped to jvmMain)
## Verifying Dependencies
### Check Module Dependencies
```bash
./gradlew :desktopApp:dependencies
./gradlew :amethyst:dependencies
```
### Check Specific Library
```bash
./gradlew dependencyInsight --dependency secp256k1
./gradlew dependencyInsight --dependency compose-ui
```
### Visualize with Build Scan
```bash
./gradlew :desktopApp:dependencies --scan
# Opens interactive dependency graph in browser
```
## Common Dependency Issues
### Issue 1: Wrong secp256k1 Variant in Desktop
**Symptom:** `UnsatisfiedLinkError: no secp256k1jni in java.library.path`
**Cause:** Desktop using Android JNI variant
**Fix:** Ensure jvmMain uses `secp256k1-kmp-jni-jvm`
### Issue 2: Compose Version Mismatch
**Symptom:** `IllegalStateException: Version mismatch`
**Cause:** Compose Multiplatform plugin vs runtime version mismatch
**Fix:** Align `composeMultiplatform` version in libs.versions.toml with Kotlin plugin
### Issue 3: Duplicate JNA Classes
**Symptom:** `DuplicateClassException: com.sun.jna.Native`
**Cause:** Both JAR and AAR JNA variants in classpath
**Fix:** Use AAR (@aar) in androidMain, JAR in jvmMain (never in shared source sets)
@@ -0,0 +1,422 @@
# Version Catalog Guide
## Overview
AmethystMultiplatform uses Gradle's version catalog (`gradle/libs.versions.toml`) to centralize dependency management. This ensures version consistency across all modules and simplifies updates.
## Structure
### sections
```toml
[versions] # Version numbers (referenced by libraries and plugins)
[libraries] # Library dependencies
[plugins] # Gradle plugins
```
## Version References
### Defining Versions
```toml
[versions]
kotlin = "2.3.0"
composeMultiplatform = "1.9.3"
okhttp = "5.3.2"
```
### Special Patterns
#### Android SDK Versions
```toml
android-compileSdk = "36"
android-minSdk = "26"
android-targetSdk = "36"
```
**Access in build.gradle.kts:**
```kotlin
compileSdk = libs.versions.android.compileSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
```
#### Version Suffixes (Git Commits)
```toml
androidKotlinGeohash = "b481c6a64e" # Jitpack commit hash
markdown = "f92ef49c9d"
```
**Why:** For GitHub dependencies via Jitpack that don't have semantic versions
## Library Declarations
### Basic Pattern
```toml
[libraries]
library-name = { group = "...", name = "...", version.ref = "..." }
```
### Examples
#### Version Reference
```toml
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
```
#### Module Reference (for multi-artifact libs)
```toml
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" }
```
#### Without Group (shorthand)
```toml
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
```
**Note:** Inherits version from BOM (compose-bom)
### BOMs (Bill of Materials)
#### AndroidX Compose BOM
```toml
[versions]
composeBom = "2025.12.01"
[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
```
**Usage in build.gradle.kts:**
```kotlin
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
implementation(libs.androidx.ui) // Version from BOM
implementation(libs.androidx.material3) // Version from BOM
```
**Benefits:**
- All AndroidX Compose artifacts use compatible versions
- Update single BOM version, not individual libraries
- Prevents version conflicts
### Platform-Specific Variants
#### secp256k1 (KMP crypto library)
```toml
secp256k1KmpJniAndroid = "0.22.0"
[libraries]
secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" }
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
```
**Critical:** All three variants MUST share the same version
#### JNA (for LibSodium)
```toml
jna = "5.18.1"
[libraries]
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
```
**Usage in build.gradle.kts:**
```kotlin
// androidMain - AAR packaging
implementation("net.java.dev.jna:jna:5.18.1@aar")
// jvmMain - JAR packaging
implementation(libs.jna)
```
**Why:** Android needs AAR, JVM needs JAR (different artifact types)
## Plugin Declarations
### Basic Pattern
```toml
[plugins]
plugin-id = { id = "...", version.ref = "..." }
```
### Examples
#### Kotlin Plugins
```toml
[versions]
kotlin = "2.3.0"
[plugins]
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' }
```
**Critical:** All Kotlin plugins MUST use the same Kotlin version
#### Android Gradle Plugin
```toml
[versions]
agp = "8.13.2"
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
```
#### Compose Multiplatform
```toml
[versions]
composeMultiplatform = "1.9.3"
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
```
### Plugin Application
```kotlin
// In build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.jetbrainsComposeCompiler)
}
```
## Usage in Build Files
### Accessing Versions
```kotlin
// Direct version access
val kotlinVersion = libs.versions.kotlin.get()
val minSdk = libs.versions.android.minSdk.get().toInt()
```
### Accessing Libraries
```kotlin
dependencies {
implementation(libs.kotlinx.coroutines.core)
api(libs.secp256k1.kmp.common)
implementation(libs.okhttp)
}
```
### Accessing Plugins
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
```
## Version Catalog Benefits
### 1. Centralized Version Management
Update once, applies everywhere:
```toml
# Change one line
kotlin = "2.3.0" → "2.4.0"
# Affects all usages
- kotlinMultiplatform plugin
- jetbrainsKotlinAndroid plugin
- kotlin-stdlib
- All Kotlin-related dependencies
```
### 2. Type-Safe Accessors
```kotlin
// Compile-time checked
implementation(libs.okhttp) // ✅ IDE autocomplete
// vs string-based (error-prone)
implementation("com.squareup.okhttp3:okhttp:5.3.2") // ❌ No autocomplete
```
### 3. Dependency Consistency
```kotlin
// All modules reference same catalog
:quartz → libs.okhttp
:commons → libs.okhttp
:desktopApp → libs.okhttp
// Same version everywhere
```
### 4. Gradle Sync Improvements
- Faster IDE sync (pre-parsed catalog)
- Better dependency resolution
- Clearer error messages
## Common Patterns
### GitHub Dependencies (Jitpack)
```toml
[versions]
markdown = "f92ef49c9d" # Git commit hash
[libraries]
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
```
**Repository config** (in settings.gradle):
```kotlin
repositories {
maven { url = "https://jitpack.io" }
}
```
### Multi-Artifact Libraries
```toml
[versions]
media3 = "1.9.0"
[libraries]
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
```
**Why:** All media3 artifacts share same version for compatibility
### Test Dependencies
```toml
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
```
## Version Update Strategy
### Check for Updates
```bash
# Using Gradle Versions Plugin (if installed)
./gradlew dependencyUpdates
# Manual check
# Browse to Maven Central for specific library
```
### Update Process
1. **Update version in catalog**
```toml
okhttp = "5.3.2" → "5.4.0"
```
2. **Test locally**
```bash
./gradlew clean build
```
3. **Check for breaking changes**
- Review library changelog
- Run full test suite
4. **Commit with clear message**
```
chore: update okhttp 5.3.2 → 5.4.0
```
### Critical Version Alignments
#### Kotlin Ecosystem
```toml
kotlin = "2.3.0"
kotlinxCoroutinesCore = "1.10.2"
kotlinxSerialization = "1.9.0"
```
**Rule:** Kotlin version must be compatible with kotlinx libraries
#### Compose Ecosystem
```toml
composeMultiplatform = "1.9.3"
composeBom = "2025.12.01"
kotlin = "2.3.0"
```
**Rule:** Compose Multiplatform → Kotlin version (see compatibility matrix)
#### AGP & Gradle
```toml
agp = "8.13.2"
# Requires Gradle 8.9+
```
**Rule:** AGP version dictates minimum Gradle version
## Troubleshooting
### Issue 1: Unresolved Reference
**Error:** `Unresolved reference: libs`
**Cause:** Gradle version < 7.0 (version catalogs not supported)
**Fix:** Upgrade Gradle in `gradle/wrapper/gradle-wrapper.properties`
### Issue 2: Library Not Found
**Error:** `Could not find com.example:library:1.0.0`
**Cause:** Repository not configured or typo in catalog
**Fix:**
1. Check repository in settings.gradle
2. Verify group/name/version in libs.versions.toml
### Issue 3: Version Conflict
**Error:** `Conflict with dependency ... and ...`
**Cause:** Different versions of same library via transitive dependencies
**Fix:**
```kotlin
configurations.all {
resolutionStrategy {
force(libs.okhttp.get().toString())
}
}
```
## Best Practices
### 1. Naming Conventions
```toml
# Hyphen-separated, hierarchical
androidx-compose-ui
androidx-compose-material3
kotlinx-coroutines-core
# Platform suffixes
secp256k1-kmp-jni-android
secp256k1-kmp-jni-jvm
```
### 2. Group Related Dependencies
```toml
# Camera APIs together
androidx-camera-core
androidx-camera-camera2
androidx-camera-view
```
### 3. Document Special Cases
```toml
# JNA requires @aar for Android (see build.gradle.kts)
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
```
### 4. Keep BOMs Updated
```toml
# Update BOM, individual libs follow
composeBom = "2025.12.01" # Latest stable
```
### 5. Test Version Updates
```bash
# Before committing
./gradlew :quartz:test
./gradlew :commons:test
./gradlew :desktopApp:run
```
@@ -0,0 +1,45 @@
#!/bin/bash
# Analyze Gradle build performance and generate report
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "🔍 Analyzing Gradle build performance..."
echo "========================================"
echo ""
# Clean build for accurate timing
echo "Running clean build with --profile..."
./gradlew clean build --profile --scan
# Find the latest profile report
PROFILE_REPORT=$(find build/reports/profile -name "*.html" -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d" ")
if [ -n "$PROFILE_REPORT" ]; then
echo ""
echo "✅ Profile report generated: $PROFILE_REPORT"
echo ""
echo "📊 Build Performance Summary:"
echo "----------------------------"
# Extract key metrics if available
if command -v jq &> /dev/null && [ -f "build/reports/profile/profile.json" ]; then
jq -r '.buildTime, .taskExecutionTime' build/reports/profile/profile.json
else
echo "Open the HTML report for detailed analysis:"
echo "file://$PWD/$PROFILE_REPORT"
fi
else
echo "⚠️ Profile report not found"
fi
echo ""
echo "💡 Build optimization tips:"
echo "- Enable Gradle daemon: org.gradle.daemon=true"
echo "- Parallel execution: org.gradle.parallel=true"
echo "- Configuration cache: org.gradle.configuration-cache=true"
echo "- Build cache: org.gradle.caching=true"
echo ""
echo "Add these to gradle.properties for faster builds"
@@ -0,0 +1,65 @@
#!/bin/bash
# Diagnose and suggest fixes for common dependency conflicts
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "🔍 Analyzing dependency conflicts..."
echo "===================================="
echo ""
# Run dependency report
echo "Generating dependency insight report..."
./gradlew dependencies --configuration runtimeClasspath > /tmp/gradle-dependencies.txt 2>&1 || true
# Check for common conflict patterns
echo ""
echo "🔎 Checking for common issues:"
echo "------------------------------"
# Check 1: Compose version conflicts
if grep -q "compose" /tmp/gradle-dependencies.txt; then
echo "✓ Compose dependencies found"
echo " Tip: Ensure Compose Multiplatform and AndroidX Compose versions align"
echo " Current project uses:"
echo " - Compose Multiplatform BOM"
echo " - AndroidX Compose BOM"
fi
# Check 2: secp256k1 variants
if grep -q "secp256k1" /tmp/gradle-dependencies.txt; then
echo "✓ secp256k1 dependencies found"
echo " Ensure correct variant:"
echo " - Android: secp256k1-kmp-jni-android"
echo " - JVM/Desktop: secp256k1-kmp-jni-jvm"
echo " - Common: secp256k1-kmp (transitive)"
fi
# Check 3: Kotlin version alignment
KOTLIN_VERSION=$(grep "kotlin =" gradle/libs.versions.toml | cut -d'"' -f2)
echo "✓ Kotlin version: $KOTLIN_VERSION"
echo " All Kotlin plugins should use the same version"
# Check 4: Multiple versions of same library
echo ""
echo "🔍 Checking for version conflicts..."
./gradlew dependencyInsight --configuration runtimeClasspath --dependency okhttp || true
echo ""
echo "💡 Common fixes:"
echo "---------------"
echo "1. Compose conflicts:"
echo " - Align compose-multiplatform plugin version with runtime"
echo " - Use BOM for AndroidX Compose to enforce consistency"
echo ""
echo "2. secp256k1 conflicts:"
echo " - Use 'api' instead of 'implementation' in source sets"
echo " - Ensure androidMain uses jni-android, jvmMain uses jni-jvm"
echo ""
echo "3. Kotlin version conflicts:"
echo " - Update all kotlin plugins to same version in libs.versions.toml"
echo " - Check for transitive Kotlin dependencies"
echo ""
echo "Run './gradlew dependencyInsight --dependency <name>' for specific conflicts"
+551
View File
@@ -0,0 +1,551 @@
---
name: nostr-expert
description: Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (57 NIPs in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent). Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details.
---
# Nostr Protocol Expert (Quartz Implementation)
Practical patterns for working with Nostr in Quartz, AmethystMultiplatform's KMP Nostr library.
## When to Use This Skill
- Implementing Nostr event types (TextNote, Reaction, Zap, etc.)
- Creating/parsing events with TagArrayBuilder DSL
- Working with event kinds and tags
- Finding NIP implementations in quartz/ codebase
- Nostr cryptography (secp256k1 signing, NIP-44 encryption)
- Bech32 encoding/decoding (npub, nsec, note formats)
- Event validation and verification
**For NIP specifications** → Use `nostr-protocol` agent
**For Quartz implementation** → Use this skill
## Quartz Architecture
Quartz organizes code by NIP number:
```
quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/
├── nip01Core/ # Core protocol (Event, Kind, Tags)
├── nip04Dm/ # Legacy DMs (deprecated)
├── nip10Notes/ # Text notes with threading
├── nip17Dm/ # Private DMs (gift wrap)
├── nip19Bech32/ # Bech32 encoding
├── nip44Encryption/ # Modern encryption (ChaCha20)
├── nip57Zaps/ # Lightning zaps
├── ... (57 NIPs total)
└── experimental/ # Draft NIPs
```
**Pattern**: `nip##<Name>/` directories contain event classes, tags, and utilities for that NIP.
**Find implementations**: Use `scripts/nip-lookup.sh <nip-number>` or see `references/nip-catalog.md`.
## Event Anatomy
### Core Structure
```kotlin
@Immutable
open class Event(
val id: HexKey, // SHA-256 hash of serialized event
val pubKey: HexKey, // Author's public key (32 bytes hex)
val createdAt: Long, // Unix timestamp
val kind: Kind, // Event kind (Int typealias)
val tags: TagArray, // Array of tag arrays
val content: String, // Event content
val sig: HexKey, // Schnorr signature (64 bytes hex)
) : IEvent
```
**Key insight**: `Event` is the base class. Specific event types (TextNoteEvent, ReactionEvent) extend it and add parsing/helper methods.
### Kind Classification
```kotlin
typealias Kind = Int
fun Kind.isEphemeral() = this in 20000..29999 // Not stored
fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999
fun Kind.isAddressable() = this in 30000..39999 // Replaceable + has d-tag
fun Kind.isRegular() = this in 1000..9999 // Stored, not replaced
```
**Pattern**: Kind determines event lifecycle and replaceability.
## Creating Events
### EventTemplate Pattern
```kotlin
fun eventTemplate(
kind: Kind,
content: String,
tags: TagArray = emptyArray()
): EventTemplate
```
**Usage**:
```kotlin
val template = eventTemplate(
kind = 1, // Text note
content = "Hello Nostr!",
tags = tagArray {
add(arrayOf("subject", "Greeting"))
}
)
// Sign with a signer
val signedEvent = signer.sign(template)
```
**Why templates?** Separates event data from signing. Templates can be signed by different signers (local keys, remote signers, hardware wallets).
### TagArrayBuilder DSL
```kotlin
fun <T : Event> tagArray(
initializer: TagArrayBuilder<T>.() -> Unit
): TagArray
```
**Methods**:
- `add(tag)` - Append tag
- `addFirst(tag)` - Prepend tag (for ordering)
- `addUnique(tag)` - Replace all tags with this name
- `remove(tagName)` - Remove by name
- `addAll(tags)` - Bulk add
**Example**:
```kotlin
val tags = tagArray<TextNoteEvent> {
add(arrayOf("e", replyToEventId, "", "reply"))
add(arrayOf("p", authorPubkey))
addUnique(arrayOf("subject", "Re: Hello"))
add(arrayOf("content-warning", "spoilers"))
}
```
**Pattern**: Fluent DSL for building tag arrays with validation and deduplication.
## Common Event Types
### TextNoteEvent (kind 1)
```kotlin
class TextNoteEvent : BaseThreadedEvent
```
**Creating**:
```kotlin
val note = eventTemplate(
kind = 1,
content = "Hello world!",
tags = tagArray {
add(arrayOf("subject", "First post"))
}
)
```
**Parsing**:
```kotlin
val event: TextNoteEvent = ...
val subject = event.subject() // Extension from nip14Subject
val mentions = event.mentions() // List of p-tags
val quotedEvents = event.quotes() // List of q-tags
```
### ReactionEvent (kind 7)
```kotlin
fun createReaction(
targetEvent: Event,
emoji: String = "+"
): EventTemplate {
return eventTemplate(
kind = 7,
content = emoji,
tags = tagArray {
add(arrayOf("e", targetEvent.id))
add(arrayOf("p", targetEvent.pubKey))
}
)
}
```
### MetadataEvent (kind 0)
```kotlin
data class UserMetadata(
val name: String?,
val displayName: String?,
val picture: String?,
val banner: String?,
val about: String?,
// ... more fields
)
fun createMetadata(metadata: UserMetadata): EventTemplate {
return eventTemplate(
kind = 0,
content = metadata.toJson() // Serialize to JSON
)
}
```
### Addressable Events (kinds 30000-40000)
```kotlin
fun createArticle(
slug: String,
title: String,
content: String
): EventTemplate {
return eventTemplate(
kind = 30023,
content = content,
tags = tagArray {
addUnique(arrayOf("d", slug)) // Unique identifier
add(arrayOf("title", title))
add(arrayOf("published_at", "${TimeUtils.now()}"))
}
)
}
```
**Key**: `d-tag` makes it addressable. Events with same kind + pubkey + d-tag replace each other.
## Tag Patterns
Tags are `Array<String>` with pattern `[name, value, ...optionalParams]`.
### Core Tags
**e-tag** (event reference):
```kotlin
add(arrayOf("e", eventId, relayHint, marker))
// marker: "reply", "root", "mention"
```
**p-tag** (pubkey reference):
```kotlin
add(arrayOf("p", pubkey, relayHint))
```
**a-tag** (addressable event):
```kotlin
add(arrayOf("a", "$kind:$pubkey:$dtag", relayHint))
```
**d-tag** (identifier for addressable events):
```kotlin
addUnique(arrayOf("d", "unique-slug"))
```
### Tag Extensions
```kotlin
// Find tags
event.tags.tagValue("subject") // First subject tag value
event.tags.allTags("p") // All p-tags
event.tags.tagValues("e") // All e-tag values
// Parse structured tags
event.tags.mapNotNull(ETag::parse) // Parse as ETag objects
```
For comprehensive tag patterns, see `references/tag-patterns.md`.
## Threading (NIP-10)
```kotlin
fun createReply(
original: TextNoteEvent,
content: String
): EventTemplate {
return eventTemplate(
kind = 1,
content = content,
tags = tagArray {
// Reply marker
add(arrayOf("e", original.id, "", "reply"))
// Root marker (original's root, or original itself)
original.rootEvent()?.let {
add(arrayOf("e", it.id, "", "root"))
} ?: add(arrayOf("e", original.id, "", "root"))
// Tag author
add(arrayOf("p", original.pubKey))
// Tag all mentioned users
original.mentions().forEach {
add(arrayOf("p", it))
}
}
)
}
```
**Pattern**: `reply` and `root` markers establish thread hierarchy.
## Cryptography
### Signing (secp256k1)
```kotlin
interface ISigner {
suspend fun sign(template: EventTemplate): Event
}
// Local key signing
class LocalSigner(private val privateKey: ByteArray) : ISigner {
override suspend fun sign(template: EventTemplate): Event {
val id = template.generateId()
val sig = Secp256k1.sign(id, privateKey)
return Event(id, pubKey, createdAt, kind, tags, content, sig)
}
}
```
**Pattern**: Signers abstract key management. Can be local, remote (NIP-46), or hardware.
### Encryption (NIP-44)
```kotlin
// Modern encryption (ChaCha20-Poly1305)
object Nip44v2 {
fun encrypt(plaintext: String, privateKey: ByteArray, pubKey: HexKey): String
fun decrypt(ciphertext: String, privateKey: ByteArray, pubKey: HexKey): String
}
// Usage
val encrypted = Nip44v2.encrypt(
plaintext = "Secret message",
privateKey = myPrivateKey,
pubKey = recipientPubKey
)
val decrypted = Nip44v2.decrypt(
ciphertext = encrypted,
privateKey = myPrivateKey,
pubKey = senderPubKey
)
```
**Pattern**: Elliptic curve Diffie-Hellman + ChaCha20-Poly1305 AEAD.
### NIP-04 (Deprecated)
```kotlin
// Legacy encryption (NIP-04, deprecated for NIP-44)
object Nip04 {
fun encrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String
fun decrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String
}
```
**Note**: Use NIP-44 (Nip44v2) for new implementations. NIP-04 has security issues.
## Bech32 Encoding (NIP-19)
```kotlin
object Nip19 {
// Encode
fun npubEncode(pubkey: HexKey): String // npub1...
fun nsecEncode(privateKey: ByteArray): String // nsec1...
fun noteEncode(eventId: HexKey): String // note1...
fun neventEncode(eventId: HexKey, relays: List<String> = emptyList()): String
fun nprofileEncode(pubkey: HexKey, relays: List<String> = emptyList()): String
fun naddrEncode(kind: Int, pubkey: HexKey, dTag: String, relays: List<String> = emptyList()): String
// Decode
fun decode(bech32: String): Nip19Result
}
sealed class Nip19Result {
data class NPub(val hex: HexKey) : Nip19Result()
data class NSec(val hex: HexKey) : Nip19Result()
data class Note(val hex: HexKey) : Nip19Result()
data class NEvent(val hex: HexKey, val relays: List<String>) : Nip19Result()
data class NProfile(val hex: HexKey, val relays: List<String>) : Nip19Result()
data class NAddr(val kind: Int, val pubkey: HexKey, val dTag: String, val relays: List<String>) : Nip19Result()
}
```
**Usage**:
```kotlin
// Encode
val npub = Nip19.npubEncode(pubkeyHex)
// Output: "npub1..."
// Decode
when (val result = Nip19.decode(npub)) {
is Nip19Result.NPub -> println("Pubkey: ${result.hex}")
is Nip19Result.NEvent -> println("Event: ${result.hex}, relays: ${result.relays}")
else -> println("Other type")
}
```
## Event Validation
```kotlin
fun Event.verify(): Boolean {
// 1. Verify ID matches content hash
val computedId = generateId()
if (id != computedId) return false
// 2. Verify signature
return Secp256k1.verify(id, sig, pubKey)
}
fun Event.generateId(): HexKey {
val serialized = serializeForId() // JSON array format
return sha256(serialized)
}
```
**Pattern**: Always verify events from untrusted sources (relays).
## Common Workflows
### Publishing an Event
```kotlin
suspend fun publishNote(content: String, signer: ISigner, relays: List<String>) {
// 1. Create template
val template = eventTemplate(kind = 1, content = content)
// 2. Sign
val event = signer.sign(template)
// 3. Verify (optional but recommended)
require(event.verify()) { "Signature verification failed" }
// 4. Publish to relays
relays.forEach { relay ->
relayClient.send(relay, event)
}
}
```
### Querying Events
```kotlin
// Subscription filter
data class Filter(
val ids: List<HexKey>? = null,
val authors: List<HexKey>? = null,
val kinds: List<Kind>? = null,
val since: Long? = null,
val until: Long? = null,
val limit: Int? = null,
val tags: Map<String, List<String>>? = null // e.g., {"#e": [eventId], "#p": [pubkey]}
)
// Usage
val filter = Filter(
authors = listOf(userPubkey),
kinds = listOf(1), // Text notes only
limit = 50
)
relayClient.subscribe(relay, filter) { event ->
// Handle incoming events
}
```
### Creating a Zap (NIP-57)
```kotlin
fun createZapRequest(
targetEvent: Event,
amountSats: Long,
comment: String = ""
): EventTemplate {
return eventTemplate(
kind = 9734, // Zap request
content = comment,
tags = tagArray {
add(arrayOf("e", targetEvent.id))
add(arrayOf("p", targetEvent.pubKey))
add(arrayOf("amount", "${amountSats * 1000}")) // millisats
add(arrayOf("relays", "wss://relay1.com", "wss://relay2.com"))
}
)
}
```
### Gift-Wrapped DMs (NIP-17)
```kotlin
fun createGiftWrappedDM(
recipientPubkey: HexKey,
message: String,
signer: ISigner
): Event {
// 1. Create sealed gossip (kind 14)
val sealedGossip = createSealedGossip(message, recipientPubkey, signer)
// 2. Wrap in gift wrap (kind 1059)
return createGiftWrap(sealedGossip, recipientPubkey, signer)
}
```
**Pattern**: Double encryption + random ephemeral keys for metadata protection.
## Finding NIPs
Use the bundled script:
```bash
# Find by NIP number
scripts/nip-lookup.sh 44
# Search by term
scripts/nip-lookup.sh encryption
scripts/nip-lookup.sh "gift wrap"
```
Or see `references/nip-catalog.md` for complete catalog.
## Bundled Resources
- **references/nip-catalog.md** - All 57 NIPs with package locations and key files
- **references/event-hierarchy.md** - Event class hierarchy, kind classifications, common types
- **references/tag-patterns.md** - Tag structure, TagArrayBuilder DSL, common tag types, parsing patterns
- **scripts/nip-lookup.sh** - Find NIP implementations by number or search term
## Quick Reference
| Task | Pattern | Location |
|------|---------|----------|
| Create event | `eventTemplate(kind, content, tags)` | nip01Core/signers/ |
| Build tags | `tagArray { add(...) }` | nip01Core/core/ |
| Sign event | `signer.sign(template)` | nip01Core/signers/ |
| Verify signature | `event.verify()` | nip01Core/core/ |
| Encrypt (NIP-44) | `Nip44v2.encrypt(...)` | nip44Encryption/ |
| Bech32 encode | `Nip19.npubEncode(...)` | nip19Bech32/ |
| Find NIP | `scripts/nip-lookup.sh <number>` | - |
## Common Event Kinds
| Kind | Type | NIP | Package |
|------|------|-----|---------|
| 0 | Metadata | 01 | nip01Core/ |
| 1 | Text note | 01, 10 | nip10Notes/ |
| 3 | Contact list | 02 | nip02FollowList/ |
| 5 | Deletion | 09 | nip09Deletions/ |
| 7 | Reaction | 25 | nip25Reactions/ |
| 1059 | Gift wrap | 59 | nip59Giftwrap/ |
| 9734 | Zap request | 57 | nip57Zaps/ |
| 9735 | Zap receipt | 57 | nip57Zaps/ |
| 10002 | Relay list | 65 | nip65RelayList/ |
| 30023 | Long-form content | 23 | nip23LongContent/ |
## Related Skills
- **nostr-protocol** - NIP specifications and protocol details
- **kotlin-expert** - Kotlin patterns (@Immutable, sealed classes, DSLs)
- **kotlin-coroutines** - Async patterns for relay communication
- **kotlin-multiplatform** - KMP patterns, expect/actual in Quartz
@@ -0,0 +1,293 @@
# Event Hierarchy & Structure
## Core Hierarchy
```
IEvent (empty interface)
└── Event (@Immutable base class)
├── BaseAddressableEvent (replaceable + addressable, has d-tag)
│ ├── BaseReplaceableEvent (kinds 10000-20000, FIXED_D_TAG = "")
│ └── [Specific addressable events - 30000-40000]
└── [Specific event implementations - all other kinds]
```
## Event Base Class
**Location**: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt`
```kotlin
@Immutable
open class Event(
val id: HexKey, // SHA-256 hash of serialized event
val pubKey: HexKey, // Author's public key (32 bytes hex)
val createdAt: Long, // Unix timestamp
val kind: Kind, // Event kind (Int typealias)
val tags: TagArray, // Array of tag arrays
val content: String, // Event content
val sig: HexKey, // schnorr signature (64 bytes hex)
) : IEvent, OptimizedSerializable
```
## Kind Classification
```kotlin
typealias Kind = Int
fun Kind.isEphemeral() = this in 20000..29999
fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999
fun Kind.isAddressable() = this in 30000..39999
fun Kind.isRegular() = this in 1000..9999
```
## Common Event Types
### Text Note (kind 1)
```kotlin
class TextNoteEvent(...) : BaseThreadedEvent(...),
EventHintProvider, AddressHintProvider, PubKeyHintProvider, SearchableEvent
// Threading support via markers: reply, root, mention
fun replyTo(): List<Note> // Direct reply targets
fun root(): Note? // Root of thread
```
### Metadata (kind 0)
```kotlin
class MetadataEvent(...) : BaseAddressableEvent(...)
// Replaceable: newest version overwrites old
// d-tag automatically set to "" for kind 0
fun name(): String?
fun displayName(): String?
fun picture(): String?
fun about(): String?
fun lnAddress(): String?
```
### Reaction (kind 7)
```kotlin
class ReactionEvent(...) : Event(...)
companion object {
const val LIKE = "+"
const val DISLIKE = "-"
fun like(reactedTo: EventHintBundle<Event>, ...)
fun dislike(reactedTo: EventHintBundle<Event>, ...)
}
```
### Zap Request/Receipt (kinds 9734, 9735)
```kotlin
class LnZapRequestEvent(...) : Event(...)
// Created by client, sent to Lightning Address
class LnZapEvent(...) : Event(...)
// Receipt from LSP, contains bolt11 + embedded zap request
val zapRequest: LnZapRequestEvent? by lazy { containedPost() }
val amount: BigDecimal? by lazy { /* parse from bolt11 */ }
```
### Long-Form Content (kind 30023)
```kotlin
class LongTextNoteEvent(...) : BaseAddressableEvent(...)
// Blog posts, articles
// Addressable via kind:pubkey:d-tag
```
### Lists (kinds 10000-30004)
```kotlin
sealed class PeopleListEvent : BaseAddressableEvent {
object MuteList : PeopleListEvent(10000)
object PinList : PeopleListEvent(10001)
object BookmarkList : PeopleListEvent(10003)
// ... 18 list types total
}
```
## Event Interfaces
### Hint Providers
Events can implement interfaces to optimize relay queries:
```kotlin
interface EventHintProvider {
fun taggedEventIds(): Set<HexKey>
fun taggedEventRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
}
interface PubKeyHintProvider {
fun taggedPubKeys(): Set<HexKey>
fun taggedPubKeyRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
}
interface AddressHintProvider {
fun taggedAddresses(): Set<Address>
fun taggedAddressRelays(): Map<Address, Set<NormalizedRelayUrl>>
}
interface SearchableEvent {
fun subject(): String?
fun isContentEncoded(): Boolean
}
```
## Event Building Pattern
### DSL Builder
```kotlin
TextNoteEvent.build(
note = "Hello Nostr",
replyingTo = eventBundle,
createdAt = TimeUtils.now()
) {
pTag(pubKey, relayHint) // Tag person
eTag(eventId, relayHint, "reply") // Tag event with marker
hashtag("nostr") // Add hashtag
alt("A short note") // Alt text
}
```
### Event Template (Low-level)
```kotlin
suspend fun eventTemplate(
kind: Kind,
content: String,
createdAt: Long,
initializer: TagArrayBuilder.() -> Unit
): EventTemplate {
val tags = TagArrayBuilder().apply(initializer).build()
return EventTemplate(kind, tags, content, createdAt)
}
// Sign with signer
val template = eventTemplate(1, "Hello", now()) { pTag(pubkey) }
val signedEvent = signer.sign(template)
```
## Addressable vs Regular Events
| Feature | Regular Event | Addressable Event |
|---------|---------------|-------------------|
| **Identifier** | Event ID (SHA-256 hash) | Address (kind:pubkey:d-tag) |
| **Replaceability** | Immutable | Newest replaces old |
| **d-tag** | Optional | Required |
| **Lookup** | By event ID | By address |
| **Example** | Text note (kind 1) | Metadata (kind 0), Long-form (kind 30023) |
```kotlin
// Regular event address
note = LocalCache.getNoteIfExists(eventId)
// Addressable event address
address = Address(kind = 30023, pubkey = authorHex, dTag = "my-article")
note = LocalCache.getAddressableNoteIfExists(address)
```
## Event Validation
```kotlin
// Verify event ID matches computed hash
fun Event.verifyId(): Boolean =
EventHasher.hashIdCheck(id, pubKey, createdAt, kind, tags, content)
// Verify signature
fun Event.verifySignature(): Boolean =
Nip01.verify(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
// Complete verification
fun Event.checkSignature() {
if (!verifyId()) throw Exception("ID mismatch")
if (!verifySignature()) throw Exception("Bad signature!")
}
```
## Event Serialization
```kotlin
// To JSON (for transmission/signing)
fun Event.toJson(): String = OptimizedJsonMapper.toJson(this)
// From JSON
fun Event.fromJson(json: String): Event = OptimizedJsonMapper.fromJson(json)
// Event ID generation (SHA-256 of canonical JSON)
fun EventHasher.hashId(
pubKey: HexKey,
createdAt: Long,
kind: Kind,
tags: TagArray,
content: String
): HexKey {
val serialized = """[0,"$pubKey",$createdAt,$kind,${tags.toJson()},"$content"]"""
return sha256(serialized.encodeToByteArray()).toHexKey()
}
```
## Event Lifecycle in LocalCache
```
Event received from relay
LocalCache.consume(event, relay, wasVerified)
getOrCreateNote(event.id) or getOrCreateAddressableNote(address)
justVerify(event) → checkSignature()
note.loadEvent(event, author, replyTo)
Update indices (replies, reactions, boosts)
refreshNewNoteObservers(note) → emit to SharedFlow
UI updates
```
## Common Event Patterns
### Reply Threading
```kotlin
// Root event (top of thread)
val rootEvent = TextNoteEvent.build("Thread root") { }
// Reply to root
val reply1 = TextNoteEvent.build("First reply", replyingTo = rootEvent) {
// Automatically adds:
// ["e", <root_id>, <relay>, "root"]
// ["e", <root_id>, <relay>, "reply"]
}
// Reply to reply (nested)
val reply2 = TextNoteEvent.build("Nested reply", replyingTo = reply1) {
// Automatically adds:
// ["e", <root_id>, <relay>, "root"]
// ["e", <reply1_id>, <relay>, "reply"]
}
```
### Replaceable Events
```kotlin
// Metadata update (kind 0) - newest wins
val metadata1 = MetadataEvent.createNew(name = "Alice", picture = "url1")
Thread.sleep(1000)
val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2")
// LocalCache keeps only metadata2 (higher createdAt)
```
### Event Deletion
```kotlin
// Delete events
val deletion = DeletionEvent.create(
deleteEvents = listOf(eventId1, eventId2),
reason = "Spam",
signer = signer
)
// LocalCache marks events as deleted, but doesn't remove (for verification)
```
## 63+ Event Classes
Full list at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip*/` - one class per event type across 60+ NIP implementations.
@@ -0,0 +1,179 @@
# NIP Catalog: 60 Standard + 8 Experimental NIPs in Quartz
## Standard NIPs by Category
### Core/Basic Protocol
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 01 | `nip01Core/` | Event.kt, Kind.kt, Tag.kt | Core protocol, event structure, kinds, tags |
| 02 | `nip02FollowList/` | ContactListEvent.kt | Follow/contact lists (kind 3) |
| 03 | `nip03Timestamp/` | OpenTimestampsAttestation.kt | Timestamps |
| 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) |
| 05 | `nip05DnsIdentifiers/` | Nip05Verifier.kt | DNS-based verification |
| 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation |
| 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) |
| 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata |
| 13 | `nip13Pow/` | ProofOfWork.kt | Proof of work |
| 14 | `nip14Subject/` | Subject tags | Subject tags for text notes |
| 17 | `nip17Dm/` | GiftWrapEvent.kt, SealedGossipEvent.kt | Private DMs (replacem
ent for NIP-04) |
| 21 | `nip21UriScheme/` | URI scheme (`nostr:`) | URI scheme parsing |
| 42 | `nip42RelayAuth/` | RelayAuthEvent.kt | Relay authentication (kind 22242) |
| 44 | `nip44Encryption/` | Nip44.kt, Nip44v2.kt | Modern encryption (ChaCha20) |
| 49 | `nip49PrivKeyEnc/` | NIP-49Ncryptsec.kt | Private key encryption format |
### Content Types
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 10 | `nip10Notes/` | TextNoteEvent.kt | Text notes with threading (kind 1) |
| 18 | `nip18Reposts/` | RepostEvent.kt, GenericRepostEvent.kt | Reposts (kind 6, 16) |
| 22 | `nip22Comments/` | CommentEvent.kt | Comments (kind 1111) |
| 23 | `nip23LongContent/` | LongTextNoteEvent.kt | Long-form content (kind 30023) |
| 25 | `nip25Reactions/` | ReactionEvent.kt | Reactions (kind 7) |
| 31 | `nip31Alts/` | Alt tags | Alt description tags |
| 36 | `nip36SensitiveContent/` | Content warnings | Content warning tags |
| 37 | `nip37Drafts/` | DraftEvent.kt | Drafts (kind 31234) |
| 50 | `nip50Search/` | Search filters | Full-text search |
### Encoding & Standards
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 19 | `nip19Bech32/` | Nip19.kt | Bech32 encoding (npub, nsec, note, nevent, nprofile, naddr) |
| 40 | `nip40Expiration/` | Expiration tags | Event expiration |
| 48 | `nip48ProxyTags/` | Proxy tags | Proxy tags for delegation |
| 62 | `nip62RequestToVanish/` | RequestToVanishEvent.kt | Request to vanish (kind 12) |
| 98 | `nip98HttpAuth/` | HTTP authorization | HTTP auth header |
### Lists & Management
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 51 | `nip51Lists/` | 18 list types | Named lists (mute, bookmarks, pins, communities, etc.) (kinds 10000-30004) |
| 65 | `nip65RelayList/` | AdvertisedRelayListEvent.kt | Relay lists (kind 10002) |
### Social & Identity
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 39 | `nip39ExtIdentities/` | External identities | External identity claims |
| 46 | `nip46RemoteSigner/` | NostrConnectEvent.kt | Remote signer protocol (bunker) |
| 47 | `nip47WalletConnect/` | Nostr Wallet Connect | Wallet connection protocol |
| 56 | `nip56Reports/` | ReportEvent.kt | Reports (kind 1984) |
| 57 | `nip57Zaps/` | LnZapEvent.kt, LnZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) |
| 58 | `nip58Badges/` | Badge events | Badge definitions & awards (kinds 30009, 8) |
| 59 | `nip59Giftwrap/` | GiftWrapEvent.kt | Gift-wrapped events for privacy |
| 75 | `nip75ZapGoals/` | ZapGoalEvent.kt | Zap goals (kind 9041) |
### Specialized Content
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 28 | `nip28PublicChat/` | ChannelCreateEvent.kt, ChannelMessageEvent.kt | Public chat channels (kinds 40-44) |
| 30 | `nip30CustomEmoji/` | EmojiUrl.kt | Custom emoji |
| 34 | `nip34Git/` | Git patch/issue events | Git repository tracking (kinds 30617, 30618, 1617, 1621, 1622, 1630, 1633) |
| 35 | `nip35Torrents/` | Torrent events | Torrent tracking |
| 52 | `nip52Calendar/` | Calendar events | Calendar time-based/date-based (kinds 31922-31925) |
| 53 | `nip53LiveActivities/` | LiveActivitiesEvent.kt | Live events/streaming (kind 30311) |
| 54 | `nip54Wiki/` | WikiNoteEvent.kt | Wiki pages (kind 30818) |
| 68 | `nip68Picture/` | Picture metadata | Picture metadata |
| 71 | `nip71Video/` | 7 video event types | Video events (kinds 34235, 35235, 1234, 1235) |
| 72 | `nip72ModCommunities/` | Community events | Moderated communities (kinds 34550, 34551, 9041) |
| 84 | `nip84Highlights/` | HighlightEvent.kt | Highlights (kind 9802) |
| 89 | `nip89AppHandlers/` | AppDefinitionEvent.kt | App recommendations (kinds 31990, 31989) |
| 90 | `nip90Dvms/` | DVM job events | Data Vending Machines (DVMs) (kinds 5000-7000) |
| 92 | `nip92IMeta/` | IMeta tags | Image metadata tags |
| 94 | `nip94FileMetadata/` | FileHeaderEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) |
| 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage |
| 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) |
| A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events |
| B7 | `nipB7Blossom/` | Blossom server URLs | Blossom file storage |
### Web/Storage/Other
| NIP | Directory | Key Files | Description |
|-----|-----------|-----------|-------------|
| 38 | `nip38UserStatus/` | StatusEvent.kt | User status (kind 30315) |
| 60 | `nip60Payment/` | Wallet events | Wallet info (kind 13194) |
| 61 | `nip61PaymentRequest/` | Nut zaps | Cashu payment requests |
| 64 | `nip64Chess/` | Chess moves | Chess move events |
| 66 | `nip66Monitoring/` | Relay monitor events | Relay monitoring |
| 67 | `nip67Invoices/` | Invoice tags | Lightning invoice tags |
| 69 | `nip69Offers/` | BOLT-12 offers | BOLT-12 offer tags |
| 70 | `nip70ProtectedEvts/` | Protected events | Protected event types |
| 73 | `nip73ExternalIds/` | External content IDs | External content identifiers |
| 78 | `nip78AppData/` | AppDataEvent.kt | Application data (kind 30078) |
| 79 | `nip79Labels/` | Label events | Labeling (kinds 1985, 1986) |
| 80-88 | Various | Various protocols | Relationship, preferences, polls, surveys, social graphs, etc. |
| 91 | `nip91Feed/` | Feed display events | Feed definitions |
| 93 | `nip93Gallery/` | Gallery events | Gallery collections |
| 95 | `nip95Storage/` | Storage event tags | Storage events |
| 97 | `nip97Nests/` | Audio rooms | Audio room events |
## Experimental NIPs (18 packages)
Located at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/`:
| Package | Description |
|---------|-------------|
| `audio/` | Audio content, track events |
| `bounties/` | Bounty/funding events |
| `decoupling/` | Decoupling setup |
| `edits/` | Event edit tracking |
| `ephemChat/` | Ephemeral encrypted chat |
| `forks/` | Fork tracking |
| `inlineMetadata/` | Inline metadata |
| `interactiveStories/` | Interactive story events |
| `limits/` | Limit enforcement |
| `medical/` | Medical data |
| `nip95/` | File storage support |
| `nipA3/` | A3 protocol extension |
| `nns/` | Nostr Name System |
| `profileGallery/` | Profile gallery lists |
| `publicMessages/` | Public message lists |
| `relationshipStatus/` | Relationship status events |
| `trustedAssertions/` | Trust/assertion events |
| `zapPolls/` | Zap-based polling |
## Quick Lookup by Kind
| Kind | Event Type | NIP |
|------|------------|-----|
| 0 | Metadata | 01 |
| 1 | Text Note | 01, 10 |
| 3 | Follow List | 02 |
| 4 | Encrypted DM (legacy) | 04 |
| 5 | Deletion | 09 |
| 6 | Repost | 18 |
| 7 | Reaction | 25 |
| 8 | Badge Award | 58 |
| 16 | Generic Repost | 18 |
| 40-44 | Channel Events | 28 |
| 1063 | File Metadata | 94 |
| 1111 | Comment | 22 |
| 1617, 1621, 1622, 1630, 1633 | Git | 34 |
| 1984 | Report | 56 |
| 1985, 1986 | Label | 79 |
| 9734 | Zap Request | 57 |
| 9735 | Zap Receipt | 57 |
| 9802 | Highlight | 84 |
| 10000-20000 | Replaceable Lists | 51 |
| 10002 | Relay List | 65 |
| 13194 | Wallet Info | 60 |
| 22242 | Relay Auth | 42 |
| 23194, 23195 | NWC Payment | 47 |
| 30000-40000 | Addressable Events | Various |
| 30009 | Badge Definition | 58 |
| 30023 | Long-Form Content | 23 |
| 30078 | App Data | 78 |
| 30311 | Live Event | 53 |
| 30315 | User Status | 38 |
| 30402 | Classifieds | 99 |
| 30818 | Wiki | 54 |
| 31234 | Draft | 37 |
| 31922-31925 | Calendar | 52 |
| 31989, 31990 | App Handlers | 89 |
| 34235, 34550-34551 | Video/Communities | 71, 72 |
| 5000-7000 | DVM Jobs | 90 |
## File Location Pattern
All NIPs located at: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip<NN><Name>/`
Example: NIP-57 → `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/`
@@ -0,0 +1,336 @@
# Tag Patterns in Quartz
Tags are the primary way events reference other events, users, and metadata in Nostr.
## Tag Structure
```kotlin
typealias Tag = Array<String> // ["tag_name", "value", "optional_param", ...]
typealias TagArray = Array<Tag>
```
**Pattern**: `[name, value, ...optionalParams]`
## TagArrayBuilder DSL
```kotlin
fun tagArray(initializer: TagArrayBuilder<T>.() -> Unit): TagArray
```
**Methods**:
- `add(tag)` - Append tag
- `addFirst(tag)` - Prepend tag
- `addUnique(tag)` - Replace all tags with this name
- `remove(tagName)` - Remove all tags with name
- `removeIf(predicate, toCompare)` - Conditional removal
**Example**:
```kotlin
val tags = tagArray<TextNoteEvent> {
add(arrayOf("e", eventId, relayHint, "reply"))
add(arrayOf("p", pubkey))
addUnique(arrayOf("subject", "Hello"))
}
```
## Core Tag Types (NIP-01)
### e-tag (Event Reference)
```kotlin
// ["e", <event-id>, <relay-hint>, <marker>]
arrayOf("e", eventId, "wss://relay.damus.io", "reply")
```
**Markers** (NIP-10):
- `root` - Root of thread
- `reply` - Direct reply target
- `mention` - Mentioned event (not reply)
**Extensions**:
```kotlin
// nip01Core/tags/
fun TagArrayBuilder.eTag(eventId: HexKey, relay: String? = null, marker: String? = null)
```
### p-tag (Pubkey Reference)
```kotlin
// ["p", <pubkey>, <relay-hint>]
arrayOf("p", pubkey, "wss://relay.damus.io")
```
**Usage**: Tag users, indicate recipients
**Extensions**:
```kotlin
fun TagArrayBuilder.pTag(pubkey: HexKey, relay: String? = null)
```
### a-tag (Addressable Event Reference)
```kotlin
// ["a", <kind>:<pubkey>:<d-tag>, <relay-hint>]
arrayOf("a", "30023:${authorPubkey}:${dtag}", "wss://relay.damus.io")
```
**Usage**: Reference replaceable/addressable events (kinds 10000-20000, 30000-40000)
**Extensions**:
```kotlin
fun TagArrayBuilder.aTag(kind: Int, pubkey: HexKey, dTag: String, relay: String? = null)
```
### d-tag (Identifier)
```kotlin
// ["d", <identifier>]
arrayOf("d", "my-article-slug")
```
**Usage**: Unique identifier for addressable events
## Common Tag Extensions
### Subject (NIP-14)
```kotlin
// nip14Subject/
fun Event.subject(): String?
fun TagArrayBuilder.subject(text: String)
```
### Content Warning (NIP-36)
```kotlin
// nip36SensitiveContent/
fun Event.contentWarning(): String?
fun TagArrayBuilder.contentWarning(reason: String = "")
```
### Expiration (NIP-40)
```kotlin
// nip40Expiration/
fun Event.expiration(): Long?
fun TagArrayBuilder.expiration(unixTimestamp: Long)
```
### Alt Description (NIP-31)
```kotlin
// nip31Alts/
fun Event.alt(): String?
fun TagArrayBuilder.alt(description: String)
```
## Specialized Tags
### Zap Tags (NIP-57)
```kotlin
// nip57Zaps/tags/
class BoltTag(val bolt11: String, val preimage: String?)
class DescriptionTag(val zapRequestJson: String)
```
### Imeta Tags (NIP-92)
```kotlin
// nip92IMeta/
class IMetaTag(val url: String, val metadata: Map<String, String>)
// Usage: Image metadata
IMetaTag("https://example.com/image.jpg", mapOf(
"m" to "image/jpeg",
"dim" to "1920x1080",
"blurhash" to "..."
))
```
### Relay Tags (NIP-65)
```kotlin
// nip65RelayList/
class RelayTag(val url: String, val type: RelayType)
enum class RelayType { READ, WRITE, BOTH }
```
## Tag Query Patterns
### Finding Tags
```kotlin
// Extension functions on TagArray
fun TagArray.firstTag(name: String): Tag?
fun TagArray.allTags(name: String): List<Tag>
fun TagArray.tagValue(name: String): String?
fun TagArray.tagValues(name: String): List<String>
```
**Example**:
```kotlin
val event: TextNoteEvent = ...
val subject = event.tags.tagValue("subject")
val mentions = event.tags.allTags("p").mapNotNull { it.getOrNull(1) }
```
### Parsing Tags
```kotlin
// Pattern: Companion object with parse methods
object ETag {
fun parse(tag: Tag): ETag? {
if (tag.getOrNull(0) != "e") return null
return ETag(
eventId = tag.getOrNull(1) ?: return null,
relay = tag.getOrNull(2),
marker = tag.getOrNull(3)
)
}
}
// Usage
val eTags = event.tags.mapNotNull(ETag::parse)
```
## Event Builder Pattern
Combining TagArrayBuilder with event creation:
```kotlin
fun createTextNote(content: String, replyTo: Event?): EventTemplate {
return eventTemplate(
kind = 1,
content = content,
tags = tagArray {
replyTo?.let {
eTag(it.id, marker = "reply")
pTag(it.pubKey)
it.rootEvent()?.let { root ->
eTag(root.id, marker = "root")
}
}
}
)
}
```
## Hint System
Tags can provide "hints" - optional relay URLs for fetching referenced content:
```kotlin
// Event references
["e", eventId, "wss://relay.example.com"] // relay hint
// Pubkey references
["p", pubkey, "wss://relay.example.com"] // relay hint
// Addressable references
["a", "30023:pubkey:dtag", "wss://relay.example.com"] // relay hint
```
**Pattern**: Third parameter (index 2) is always the relay hint
## Tag Validation
```kotlin
// Common validations
fun validateETag(tag: Tag): Boolean {
return tag.getOrNull(0) == "e" && tag.getOrNull(1)?.isValidHex() == true
}
fun validatePTag(tag: Tag): Boolean {
return tag.getOrNull(0) == "p" && tag.getOrNull(1)?.isValidHex() == true
}
```
## Performance Patterns
### Tag Indexing
```kotlin
// TagArrayBuilder keeps an index by tag name
private val tagList = mutableMapOf<String, MutableList<Tag>>()
// Fast lookup by name
fun remove(tagName: String) {
tagList.remove(tagName)
}
```
### Lazy Parsing
```kotlin
// Don't parse all tags upfront
class TextNoteEvent(...) {
private val _mentions by lazy {
tags.mapNotNull(PTag::parse)
}
fun mentions() = _mentions
}
```
## Common Workflows
### Creating a Reply
```kotlin
fun replyTo(original: TextNoteEvent, content: String): EventTemplate {
return eventTemplate(
kind = 1,
content = content,
tags = tagArray {
// Reply to this event
eTag(original.id, marker = "reply")
// Copy root marker if exists, or mark original as root
original.rootEvent()?.let {
eTag(it.id, marker = "root")
} ?: eTag(original.id, marker = "root")
// Tag author
pTag(original.pubKey)
// Tag all mentioned users
original.mentions().forEach { pTag(it) }
}
)
}
```
### Creating a Reaction
```kotlin
fun createReaction(targetEvent: Event, emoji: String): EventTemplate {
return eventTemplate(
kind = 7,
content = emoji,
tags = tagArray {
eTag(targetEvent.id)
pTag(targetEvent.pubKey)
}
)
}
```
### Creating an Addressable Event
```kotlin
fun createArticle(title: String, content: String, slug: String): EventTemplate {
return eventTemplate(
kind = 30023,
content = content,
tags = tagArray {
addUnique(arrayOf("d", slug)) // Unique identifier
add(arrayOf("title", title))
add(arrayOf("published_at", "${TimeUtils.now()}"))
}
)
}
```
## Quick Reference
| Tag | NIP | Usage | Example |
|-----|-----|-------|---------|
| e | 01 | Event reference | `["e", eventId, relay, marker]` |
| p | 01 | Pubkey reference | `["p", pubkey, relay]` |
| a | 01 | Addressable event | `["a", "kind:pubkey:d"]` |
| d | 01 | Identifier | `["d", "unique-id"]` |
| subject | 14 | Subject line | `["subject", "Hello"]` |
| content-warning | 36 | Content warning | `["content-warning", "nsfw"]` |
| expiration | 40 | Expiration time | `["expiration", "1234567890"]` |
| bolt11 | 57 | Lightning invoice | `["bolt11", "lnbc..."]` |
| imeta | 92 | Media metadata | `["imeta", "url", "m", "image/jpeg"]` |
| relay | 65 | User relays | `["relay", "wss://...", "read"]` |
## Resources
- Tag builders: `quartz/src/commonMain/.../nip01Core/tags/`
- Tag extensions: Look for `TagArrayExt.kt`, `TagArrayBuilderExt.kt` in each NIP package
- Event parsing: Each event class has tag parsing methods
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Find NIP implementation files by NIP number or search term
set -e
QUARTZ_PATH="${QUARTZ_PATH:-./quartz/src/commonMain/kotlin/com/vitorpamplona/quartz}"
if [ $# -eq 0 ]; then
echo "Usage: $0 <nip-number|search-term>"
echo ""
echo "Examples:"
echo " $0 01 # Find NIP-01 files"
echo " $0 44 # Find NIP-44 files"
echo " $0 encryption # Search for 'encryption' in NIP packages"
echo ""
echo "Set QUARTZ_PATH to override default quartz location"
exit 1
fi
SEARCH_TERM="$1"
# Check if it's a number (NIP number)
if [[ "$SEARCH_TERM" =~ ^[0-9]+$ ]]; then
# Pad to 2 digits
NIP_NUM=$(printf "%02d" "$SEARCH_TERM")
echo "Searching for NIP-$NIP_NUM implementation..."
echo "================================================"
echo ""
# Find directories matching nip##*
find "$QUARTZ_PATH" -type d -name "nip${NIP_NUM}*" | while read -r dir; do
echo "📁 $(basename "$dir")/"
find "$dir" -name "*.kt" -type f | while read -r file; do
rel_path="${file#$QUARTZ_PATH/}"
echo " └─ $rel_path"
done
echo ""
done
else
# Text search
echo "Searching for '$SEARCH_TERM' in NIP packages..."
echo "================================================"
echo ""
find "$QUARTZ_PATH" -type d -name "nip*" | while read -r dir; do
if grep -r -l -i "$SEARCH_TERM" "$dir" --include="*.kt" 2>/dev/null | head -1 > /dev/null; then
echo "📁 $(basename "$dir")/"
grep -r -l -i "$SEARCH_TERM" "$dir" --include="*.kt" 2>/dev/null | while read -r file; do
rel_path="${file#$QUARTZ_PATH/}"
matches=$(grep -c -i "$SEARCH_TERM" "$file" 2>/dev/null || echo "0")
echo " └─ $rel_path ($matches matches)"
done
echo ""
fi
done
fi
echo "Done."