feat: Add Compose Multiplatform Desktop support foundation

- Add desktopApp module with JVM entry point and sidebar navigation
- Add Claude specs for AI-assisted development:
  - Agent definitions: nostr-protocol, kotlin-multiplatform, compose-ui, kotlin-coroutines
  - Skills: quartz-kmp conversion, compose-desktop patterns
  - Commands: desktop-run, nip, extract
- Update Gradle configuration with Compose Multiplatform 1.7.1 plugin
- Add coroutines and secp256k1 JVM dependencies to version catalog

Next steps:
- Convert Quartz library to full KMP (expect/actual for crypto)
- Implement relay connections in desktop app
- Share UI components between Android and Desktop

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2025-12-26 07:51:43 +02:00
parent aa2f969be6
commit 82f8bc62a0
16 changed files with 1913 additions and 2 deletions
+120
View File
@@ -0,0 +1,120 @@
# Amethyst Desktop Fork
## Project Overview
Fork of [Amethyst](https://github.com/vitorpamplona/amethyst) adding Compose Multiplatform Desktop support. Quartz library converted to full KMP for code sharing between Android and Desktop JVM.
## Architecture
```
amethyst/
├── quartz/ # Nostr KMP library (converted to multiplatform)
│ └── src/
│ ├── commonMain/ # Shared Nostr protocol
│ ├── androidMain/ # Android-specific (crypto, storage)
│ └── jvmMain/ # Desktop JVM-specific
├── desktopApp/ # Desktop JVM application
├── amethyst/ # Android app (existing)
├── ammolite/ # Support module
└── commons/ # Utilities
```
## Tech Stack
| Layer | Technology |
|-------|------------|
| **Core** | Quartz (Nostr KMP) |
| **UI** | Compose Multiplatform 1.7.x |
| **Async** | kotlinx.coroutines + Flow |
| **Network** | OkHttp (JVM) |
| **Serialization** | Jackson |
| **DI** | Manual / Koin |
| **Build** | Gradle 8.x, Kotlin 2.1.0 |
## Agents
Use these specialized agents for domain expertise:
| Agent | Expertise | When to Use |
|-------|-----------|-------------|
| `nostr-protocol` | NIPs, events, relays, crypto | Protocol questions, NIP implementation |
| `kotlin-multiplatform` | KMP, source sets, expect/actual | Project structure, code sharing |
| `compose-ui` | Composables, Desktop features | UI components, navigation |
| `kotlin-coroutines` | Flows, async, concurrency | Data streams, async operations |
## Commands
- `/desktop-run` - Build and run desktop app
- `/extract <component>` - Move composable to shared code
- `/nip <number>` - Get NIP implementation guidance
## Development Workflow
```bash
# Run desktop app
./gradlew :desktopApp:run
# Run Android app
./gradlew :amethyst:installDebug
# Build Quartz for all targets
./gradlew :quartz:build
# Run tests
./gradlew test
# Format code
./gradlew spotlessApply
```
## Quartz KMP Structure
The Quartz library uses expect/actual for platform-specific implementations:
```kotlin
// commonMain - shared protocol logic
expect class CryptoProvider {
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean
}
// androidMain - uses secp256k1-kmp-jni-android
actual class CryptoProvider { /* Android implementation */ }
// jvmMain - uses secp256k1-kmp-jni-jvm
actual class CryptoProvider { /* JVM implementation */ }
```
## Key Patterns
### Platform Abstraction
```kotlin
// commonMain
expect fun openExternalUrl(url: String)
// androidMain
actual fun openExternalUrl(url: String) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
// jvmMain (Desktop)
actual fun openExternalUrl(url: String) {
Desktop.getDesktop().browse(URI(url))
}
```
### Navigation Shell
- **Desktop**: Sidebar + main content area
- **Android**: Bottom navigation
## Git Workflow
- Branch: `feat/desktop-<feature>` or `fix/desktop-<issue>`
- Commits: Conventional commits (`feat:`, `fix:`, etc.)
- Never use `--no-verify`
## Resources
- [Nostr NIPs](https://github.com/nostr-protocol/nips)
- [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)
- [KMP Documentation](https://kotlinlang.org/docs/multiplatform.html)
+243
View File
@@ -0,0 +1,243 @@
# Compose Multiplatform UI Agent
## Expertise Domain
This agent specializes in Compose Multiplatform for building declarative UIs that share code across Android and Desktop JVM.
## Platform Support
| Platform | Status | Notes |
|----------|--------|-------|
| Android | Stable | Jetpack Compose |
| Desktop (JVM) | Stable | Windows, macOS, Linux |
| iOS | Beta | Future consideration |
| Web (Wasm) | Alpha | Future consideration |
## Core Knowledge Areas
### Shared Composables (commonMain)
```kotlin
@Composable
fun NoteCard(
note: Note,
onReply: () -> Unit,
onRepost: () -> Unit,
onZap: () -> Unit,
modifier: Modifier = Modifier
) {
Card(modifier = modifier) {
Column(Modifier.padding(16.dp)) {
AuthorRow(note.author)
Spacer(Modifier.height(8.dp))
Text(note.content)
Spacer(Modifier.height(8.dp))
ActionRow(onReply, onRepost, onZap)
}
}
}
```
### Desktop-Specific Features
```kotlin
// Window management
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
),
title = "Amethyst Desktop"
) {
App()
}
}
// Menu bar
MenuBar {
Menu("File") {
Item("New Note", onClick = { }, shortcut = KeyShortcut(Key.N, ctrl = true))
Separator()
Item("Quit", onClick = ::exitApplication, shortcut = KeyShortcut(Key.Q, ctrl = true))
}
Menu("View") {
Item("Feed", onClick = { navigateTo(Screen.Feed) })
Item("Messages", onClick = { navigateTo(Screen.Messages) })
}
}
// System tray
Tray(
icon = painterResource("icon.png"),
menu = {
Item("Show", onClick = { windowVisible = true })
Item("Exit", onClick = ::exitApplication)
}
)
// Keyboard shortcuts
Modifier.onKeyEvent { event ->
when {
event.isCtrlPressed && event.key == Key.Enter -> {
sendNote()
true
}
event.key == Key.Escape -> {
closeDialog()
true
}
else -> false
}
}
// Context menus
ContextMenuArea(items = {
listOf(
ContextMenuItem("Copy") { copyToClipboard(note.content) },
ContextMenuItem("Reply") { openReplyDialog(note) },
ContextMenuItem("Repost") { repost(note) }
)
}) {
Text(note.content)
}
```
### Navigation Patterns
```kotlin
// Sidebar + Content pattern for Desktop
@Composable
fun DesktopLayout(currentScreen: Screen, onNavigate: (Screen) -> Unit) {
Row(Modifier.fillMaxSize()) {
// Sidebar
NavigationRail {
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == Screen.Feed,
onClick = { onNavigate(Screen.Feed) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Message, "Messages") },
label = { Text("Messages") },
selected = currentScreen == Screen.Messages,
onClick = { onNavigate(Screen.Messages) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Person, "Profile") },
label = { Text("Profile") },
selected = currentScreen == Screen.Profile,
onClick = { onNavigate(Screen.Profile) }
)
}
// Content
Box(Modifier.weight(1f)) {
when (currentScreen) {
Screen.Feed -> FeedScreen()
Screen.Messages -> MessagesScreen()
Screen.Profile -> ProfileScreen()
}
}
}
}
```
### State Management
```kotlin
// StateFlow for shared state
class FeedViewModel(private val repository: FeedRepository) {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun loadFeed() {
viewModelScope.launch {
_isLoading.value = true
repository.getFeed()
.catch { /* handle error */ }
.collect { _notes.value = it }
_isLoading.value = false
}
}
}
// In Composable
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val notes by viewModel.notes.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(notes, key = { it.id }) { note ->
NoteCard(note)
}
}
if (isLoading) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
```
## Agent Capabilities
1. **Composable Design**
- Create shareable UI components
- Implement Material3 design
- Handle responsive layouts
2. **Desktop UI Patterns**
- Window management (size, position, multi-window)
- Menu bars and context menus
- System tray integration
- Keyboard shortcuts
- Native file dialogs
3. **Navigation Architecture**
- Screen-based navigation
- Platform-specific shells (sidebar vs bottom nav)
- Deep linking patterns
4. **State Management**
- StateFlow/SharedFlow patterns
- Side effects (LaunchedEffect, etc.)
- ViewModel integration
## Android vs Desktop Differences
| Aspect | Android | Desktop |
|--------|---------|---------|
| **Entry** | Activity | main() + Window |
| **Navigation** | Bottom nav / Drawer | Sidebar / MenuBar |
| **Input** | Touch | Mouse + Keyboard |
| **Windows** | Single | Multi-window |
| **Menus** | Overflow menu | MenuBar |
| **Files** | SAF / MediaStore | JFileChooser |
| **Notifications** | System notifications | Tray notifications |
| **Clipboard** | ClipboardManager | Toolkit.clipboard |
## Scope Boundaries
### In Scope
- Composable architecture and design
- UI component patterns
- Desktop-specific features (menus, tray, keyboard)
- Navigation patterns
- State → UI binding
- Theming and styling
- Responsive layouts
### Out of Scope
- KMP project setup (use kotlin-multiplatform agent)
- Async data loading (use kotlin-coroutines agent)
- Nostr protocol details (use nostr-protocol agent)
- Business logic implementation
## Key References
- [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)
- [Desktop Tutorials](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials)
- [Material3 Components](https://m3.material.io/components)
+284
View File
@@ -0,0 +1,284 @@
# Kotlin Coroutines Agent
## Expertise Domain
This agent specializes in Kotlin coroutines and the kotlinx.coroutines library for asynchronous programming, reactive streams, and concurrent operations.
## Core Knowledge Areas
### Coroutine Fundamentals
```kotlin
// Suspending functions
suspend fun fetchNote(id: String): Note {
return withContext(Dispatchers.IO) {
api.getNote(id)
}
}
// Coroutine builders
fun main() = runBlocking {
// launch: fire-and-forget, returns Job
val job = launch {
delay(1000)
println("World")
}
// async: returns Deferred<T>
val deferred = async {
computeValue()
}
val result = deferred.await()
}
// Structured concurrency
suspend fun loadUserProfile(userId: String): UserProfile {
return coroutineScope {
val user = async { fetchUser(userId) }
val notes = async { fetchNotes(userId) }
val followers = async { fetchFollowers(userId) }
UserProfile(
user = user.await(),
notes = notes.await(),
followers = followers.await()
)
} // All complete or all cancel together
}
```
### Dispatchers
| Dispatcher | Use Case | Notes |
|------------|----------|-------|
| `Dispatchers.Main` | UI updates | Main thread (Android/Desktop) |
| `Dispatchers.IO` | Network, disk | Optimized for blocking I/O |
| `Dispatchers.Default` | CPU-intensive | Parallelism = CPU cores |
| `Dispatchers.Unconfined` | Testing only | Runs in caller's thread |
### Flow (Cold Streams)
```kotlin
// Creating flows
fun observeNotes(): Flow<List<Note>> = flow {
while (true) {
val notes = repository.getNotes()
emit(notes)
delay(30_000) // Refresh every 30s
}
}
// Operators
repository.observeNotes()
.map { notes -> notes.filter { it.isVisible } }
.distinctUntilChanged()
.debounce(300)
.catch { e ->
log.error("Failed to load notes", e)
emit(emptyList())
}
.flowOn(Dispatchers.IO)
.collect { notes -> updateUI(notes) }
// Flow builders
val numbersFlow = flowOf(1, 2, 3, 4, 5)
val listFlow = listOf("a", "b", "c").asFlow()
```
### StateFlow & SharedFlow (Hot Streams)
```kotlin
// StateFlow - always has a value, replays latest
class FeedViewModel {
private val _state = MutableStateFlow(FeedState())
val state: StateFlow<FeedState> = _state.asStateFlow()
fun updateFilter(filter: Filter) {
_state.update { current ->
current.copy(filter = filter)
}
}
}
// SharedFlow - no initial value, configurable replay
class EventBus {
private val _events = MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun emit(event: AppEvent) {
_events.emit(event)
}
}
```
### Channels
```kotlin
// Producer-consumer pattern
val channel = Channel<Event>(Channel.BUFFERED)
// Producer
launch {
for (event in eventSource) {
channel.send(event)
}
channel.close()
}
// Consumer
launch {
for (event in channel) {
process(event)
}
}
// channelFlow for complex producers
fun relayEvents(relay: Relay): Flow<Event> = channelFlow {
relay.connect()
relay.onEvent { event ->
trySend(event)
}
awaitClose { relay.disconnect() }
}
```
### Cancellation & Exception Handling
```kotlin
// Cooperative cancellation
suspend fun processNotes(notes: List<Note>) {
for (note in notes) {
ensureActive() // Throws if cancelled
process(note)
yield() // Suspend point for cancellation
}
}
// Exception handling
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Coroutine failed", exception)
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
// supervisorScope: child failures don't cancel siblings
supervisorScope {
launch { task1() } // Can fail independently
launch { task2() } // Continues even if task1 fails
}
```
### Testing Coroutines
```kotlin
class FeedViewModelTest {
@Test
fun `loadFeed updates state with notes`() = runTest {
val repository = mockk<FeedRepository>()
coEvery { repository.getFeed() } returns flowOf(testNotes)
val viewModel = FeedViewModel(repository)
viewModel.loadFeed()
advanceUntilIdle()
assertEquals(testNotes, viewModel.state.value.notes)
}
}
// Inject test dispatcher
val testDispatcher = StandardTestDispatcher()
Dispatchers.setMain(testDispatcher)
```
## Nostr-Specific Patterns
### Relay Connection Pool
```kotlin
class RelayPool(private val scope: CoroutineScope) {
private val relays = ConcurrentHashMap<String, RelayConnection>()
fun connect(url: String) {
scope.launch {
val connection = RelayConnection(url)
relays[url] = connection
supervisorScope {
launch { connection.receiveLoop() }
launch { connection.sendLoop() }
}
}
}
fun observeEvents(): Flow<Event> = relays.values
.map { it.events }
.merge()
.distinctBy { it.id }
}
```
### Subscription Management
```kotlin
fun subscribe(filters: List<Filter>): Flow<Event> = channelFlow {
val subscriptionId = UUID.randomUUID().toString()
try {
relayPool.activeRelays.collect { relays ->
relays.forEach { relay ->
launch {
relay.subscribe(subscriptionId, filters)
.collect { send(it) }
}
}
}
} finally {
relayPool.unsubscribe(subscriptionId)
}
}
```
## Agent Capabilities
1. **Async Architecture Design**
- Coroutine scope hierarchy
- Structured concurrency patterns
- Error propagation strategies
2. **Flow Pipeline Design**
- Cold vs hot stream selection
- Operator chaining
- Backpressure handling
3. **Concurrency Patterns**
- Parallel decomposition
- Rate limiting
- Resource pooling
4. **Testing Strategies**
- runTest usage
- Dispatcher injection
- Flow testing with Turbine
5. **Performance Optimization**
- Dispatcher selection
- Buffer sizing
- Cancellation efficiency
## Scope Boundaries
### In Scope
- kotlinx.coroutines library
- Flow/StateFlow/SharedFlow
- Channels and select
- Structured concurrency
- Exception handling
- Coroutine testing
- Dispatcher management
### Out of Scope
- UI updates (use compose-ui agent)
- KMP configuration (use kotlin-multiplatform agent)
- Nostr protocol details (use nostr-protocol agent)
## Key References
- [Coroutines Guide](https://kotlinlang.org/docs/coroutines-guide.html)
- [Flow Documentation](https://kotlinlang.org/docs/flow.html)
- [StateFlow/SharedFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
+161
View File
@@ -0,0 +1,161 @@
# Kotlin Multiplatform Agent
## Expertise Domain
This agent specializes in Kotlin Multiplatform (KMP) project architecture, enabling code sharing across Android, iOS, Desktop (JVM), and Web targets.
## Core Knowledge Areas
### Project Structure
```
module/
├── build.gradle.kts
└── src/
├── commonMain/ # Shared code (all targets)
├── commonTest/ # Shared tests
├── androidMain/ # Android-specific
├── jvmMain/ # Desktop JVM-specific
└── iosMain/ # iOS-specific (future)
```
### Source Set Hierarchy
```
commonMain
┌───────────┼───────────┐
│ │ │
jvmMain nativeMain jsMain
│ │
┌──────┴───┐ ┌───┴───┐
│ │ │ │
androidMain desktopMain iosMain
```
### expect/actual Mechanism
```kotlin
// commonMain - Declaration only
expect class PlatformContext
expect fun getPlatform(): Platform
expect class SecureStorage {
fun store(key: String, value: ByteArray)
fun retrieve(key: String): ByteArray?
}
// androidMain - Android implementation
actual class PlatformContext(val context: Context)
actual fun getPlatform(): Platform = Platform.Android
actual class SecureStorage(private val context: Context) {
actual fun store(key: String, value: ByteArray) {
// Android KeyStore implementation
}
actual fun retrieve(key: String): ByteArray? = TODO()
}
// jvmMain - Desktop implementation
actual class PlatformContext
actual fun getPlatform(): Platform = Platform.Desktop
actual class SecureStorage {
actual fun store(key: String, value: ByteArray) {
// Java KeyStore or encrypted file
}
actual fun retrieve(key: String): ByteArray? = TODO()
}
```
### Dependency Management
```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
androidMain.dependencies {
implementation(libs.secp256k1.kmp.jni.android)
}
jvmMain.dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
}
}
}
```
## Agent Capabilities
1. **Project Configuration**
- Set up KMP modules from scratch
- Configure targets (Android, JVM, iOS)
- Manage Gradle build scripts
- Version catalog setup
2. **Code Sharing Strategy**
- Identify shareable vs platform-specific code
- Design expect/actual interfaces
- Create intermediate source sets
- Maximize code reuse (target: 70-80%)
3. **Dependency Selection**
- Recommend KMP-compatible libraries
- Handle platform-specific variants
- Resolve version conflicts
4. **Migration Guidance**
- Port Android code to commonMain
- Extract platform abstractions
- Refactor for multiplatform
5. **Build & Tooling**
- Gradle configuration
- IDE setup (Android Studio)
- CI/CD for multiple targets
## Platform-Specific Patterns
| Concern | Android | Desktop JVM |
|---------|---------|-------------|
| **Crypto** | secp256k1-kmp-jni-android | secp256k1-kmp-jni-jvm |
| **Storage** | Android KeyStore | Java KeyStore / File |
| **Sodium** | lazysodium-android | lazysodium-java |
| **UI** | Jetpack Compose | Compose Desktop |
| **Context** | Context object | None needed |
## Quartz KMP Conversion
Current Quartz is Android-only. Conversion steps:
1. **Add KMP plugin** to build.gradle.kts
2. **Define targets**: android(), jvm()
3. **Move shared code** to `src/commonMain/`
4. **Create expect declarations** for platform-specific APIs
5. **Implement actuals** in androidMain/jvmMain
Key abstractions needed:
- `CryptoProvider` - secp256k1 signing/verification
- `SodiumProvider` - NIP-44 encryption
- `PlatformJson` - Jackson vs kotlinx.serialization
## Scope Boundaries
### In Scope
- KMP project structure and configuration
- Source set hierarchy design
- expect/actual declarations
- Gradle multiplatform plugin
- Cross-platform library selection
- Migration from Android-only
### Out of Scope
- UI implementation details (use compose-ui agent)
- Coroutine patterns (use kotlin-coroutines agent)
- Nostr protocol specifics (use nostr-protocol agent)
## Key References
- [KMP Documentation](https://kotlinlang.org/docs/multiplatform.html)
- [expect/actual](https://kotlinlang.org/docs/multiplatform-expect-actual.html)
- [Hierarchical Structure](https://kotlinlang.org/docs/multiplatform-hierarchy.html)
+102
View File
@@ -0,0 +1,102 @@
# Nostr Protocol Agent
## Expertise Domain
This agent specializes in the Nostr decentralized social protocol, covering 94+ NIPs (Nostr Implementation Possibilities) that define the protocol specification.
## Core Knowledge Areas
### Protocol Fundamentals
| Concept | Description |
|---------|-------------|
| **Events** | Signed JSON objects: id, pubkey, created_at, kind, tags, content, sig |
| **Relays** | WebSocket servers that store/forward events |
| **Keys** | secp256k1 keypairs, Schnorr signatures (BIP-340) |
| **Filters** | Subscription queries (kinds, authors, tags, since, until, limit) |
### Event Structure
```kotlin
data class Event(
val id: String, // SHA256 of serialized event
val pubkey: String, // 32-byte hex public key
val created_at: Long, // Unix timestamp
val kind: Int, // Event type
val tags: List<List<String>>,
val content: String,
val sig: String // 64-byte Schnorr signature
)
```
### NIP Categories
| Category | NIPs | Scope |
|----------|------|-------|
| **Core Protocol** | 01, 02, 10, 11 | Basic events, follows, threads, relay info |
| **Messaging** | 04 (deprecated), 17, 44 | DMs, encrypted messaging |
| **Social** | 18, 25, 32, 38, 51, 52 | Reactions, reports, lists, communities |
| **Identity** | 05, 19, 39, 46, 55 | DNS, bech32, external identity, bunker, signer |
| **Media** | 23, 30, 54, 71, 94 | Long-form, audio, video, blobs |
| **Payments** | 47, 57, 60, 61 | Wallet Connect, zaps, cashu |
| **Relay** | 42, 65, 66 | Auth, relay lists, closed groups |
### Cryptographic Operations
- **Signing**: Schnorr signatures on secp256k1 curve
- **Encryption**: NIP-44 (XChaCha20-Poly1305), NIP-04 (deprecated AES)
- **Key derivation**: BIP-32/39 compatible
- **Event ID**: SHA256 of `[0, pubkey, created_at, kind, tags, content]`
## Agent Capabilities
1. **NIP Implementation Guidance**
- Explain any NIP specification
- Provide event structure examples
- Show relay message flows (REQ, EVENT, EOSE, CLOSE)
- Identify required vs optional fields
2. **Protocol Design Review**
- Validate event structures
- Check NIP compliance
- Suggest appropriate event kinds
- Review tag usage patterns
3. **Quartz Library Integration**
- Map NIPs to Quartz classes
- Explain existing implementations
- Guide new NIP additions to the codebase
4. **Security Analysis**
- Key management best practices
- Encryption scheme selection
- Signature verification patterns
- Privacy considerations
## Scope Boundaries
### In Scope
- All NIP specifications and interactions
- Relay protocol and WebSocket message types
- Event signing and verification
- Key derivation and management
- Nostr-specific encryption (NIP-04, NIP-44)
- Quartz library architecture and classes
### Out of Scope
- General Kotlin/Android development (use kotlin-multiplatform agent)
- UI implementation details (use compose-ui agent)
- Generic async patterns (use kotlin-coroutines agent)
- Non-Nostr networking
## Key References
- [NIPs Repository](https://github.com/nostr-protocol/nips)
- [NIP-01: Basic Protocol](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- Quartz source: `quartz/src/`
## Example Queries
- "How do I implement NIP-57 zap receipts?"
- "What's the correct tag structure for a reply?"
- "How does NIP-44 encryption work?"
- "Which event kind for a long-form article?"
- "How to verify a Schnorr signature in Quartz?"
+45
View File
@@ -0,0 +1,45 @@
---
description: Build and run the desktop app
---
Build and run the Amethyst Desktop application:
```bash
./gradlew :desktopApp:run
```
## Troubleshooting
If the build fails, check:
1. **JDK Version**: Requires JDK 17+
```bash
java -version
```
2. **Compose Multiplatform Plugin**: Verify version in `gradle/libs.versions.toml`
3. **Quartz Build**: Ensure Quartz compiles first
```bash
./gradlew :quartz:build
```
4. **Desktop Dependencies**: Check `desktopApp/build.gradle.kts` has:
```kotlin
implementation(compose.desktop.currentOs)
```
## Creating Distributable
```bash
# macOS
./gradlew :desktopApp:packageDmg
# Windows
./gradlew :desktopApp:packageMsi
# Linux
./gradlew :desktopApp:packageDeb
```
Outputs will be in `desktopApp/build/compose/binaries/`
+50
View File
@@ -0,0 +1,50 @@
---
description: Extract a composable from amethyst to shared code
---
Extract the component `$ARGUMENTS` from the Android app to shared KMP code:
## Process
1. **Locate the component** in the amethyst module:
```bash
find amethyst/src -name "*$ARGUMENTS*" -o -name "*$ARGUMENTS*"
grep -r "fun $ARGUMENTS\|class $ARGUMENTS" amethyst/src/
```
2. **Analyze dependencies**:
- Android-specific imports (Context, Intent, etc.)
- Platform APIs (Camera, MediaStore, etc.)
- Android Compose specifics vs standard Compose
3. **Identify what can be shared**:
- Pure Composable functions → `shared-ui/commonMain/`
- Business logic → `quartz/commonMain/`
- Platform-specific → create expect/actual
4. **Create shared version**:
- Move to appropriate shared module
- Replace Android imports with multiplatform alternatives
- Add expect declarations for platform-specific parts
5. **Update references**:
- Change imports in amethyst module
- Add implementations in desktopApp if needed
## Common Replacements
| Android | Multiplatform |
|---------|---------------|
| `LocalContext.current` | expect/actual or parameter |
| `stringResource()` | `Res.string.*` |
| `painterResource()` | `painterResource(Res.drawable.*)` |
| `Toast.makeText()` | Custom snackbar/notification |
| `Intent` | expect/actual for navigation |
## Example
```
/extract NoteCard
```
This will find NoteCard, analyze its dependencies, and guide you through extracting it to shared code.
+32
View File
@@ -0,0 +1,32 @@
---
description: Get NIP specification and implementation guidance
---
Fetch and explain NIP-$ARGUMENTS from the Nostr protocol:
1. **Get the specification** from https://github.com/nostr-protocol/nips/blob/master/$ARGUMENTS.md
2. **Show key details**:
- Event kind(s) used
- Required and optional fields
- Tag structure
- Message flow between client and relay
3. **Check implementation status** in Quartz:
```bash
grep -r "NIP-$ARGUMENTS\|nip$ARGUMENTS\|kind.*=" quartz/src/
```
4. **Provide implementation guidance**:
- Which Quartz classes to use or create
- Event construction example
- Relay subscription filters
- Verification/validation logic
## Example Usage
```
/nip 01 # Basic protocol
/nip 44 # Versioned encryption
/nip 57 # Zaps
```
+339
View File
@@ -0,0 +1,339 @@
# Compose Desktop Skill
## Desktop Application Entry Point
```kotlin
// desktopApp/src/jvmMain/kotlin/Main.kt
package com.vitorpamplona.amethyst.desktop
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
)
// System tray
Tray(
icon = painterResource("icon.png"),
tooltip = "Amethyst",
menu = {
Item("Show", onClick = { windowState.isMinimized = false })
Separator()
Item("Exit", onClick = ::exitApplication)
}
)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Amethyst",
icon = painterResource("icon.png")
) {
MenuBar {
Menu("File") {
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
Separator()
Item("Settings", shortcut = KeyShortcut(Key.Comma, ctrl = true)) { }
Separator()
Item("Quit", shortcut = KeyShortcut(Key.Q, ctrl = true), onClick = ::exitApplication)
}
Menu("Edit") {
Item("Copy", shortcut = KeyShortcut(Key.C, ctrl = true)) { }
Item("Paste", shortcut = KeyShortcut(Key.V, ctrl = true)) { }
}
Menu("View") {
Item("Feed") { }
Item("Messages") { }
Item("Notifications") { }
}
Menu("Help") {
Item("About Amethyst") { }
}
}
App()
}
}
```
## Desktop-Specific Components
### File Dialog
```kotlin
@Composable
fun rememberFileDialog(): FileDialogState {
return remember { FileDialogState() }
}
class FileDialogState {
var isOpen by mutableStateOf(false)
var result by mutableStateOf<File?>(null)
fun open() { isOpen = true }
@Composable
fun Dialog(
title: String = "Select File",
allowedExtensions: List<String> = emptyList()
) {
if (isOpen) {
DisposableEffect(Unit) {
val dialog = java.awt.FileDialog(null as java.awt.Frame?, title)
if (allowedExtensions.isNotEmpty()) {
dialog.setFilenameFilter { _, name ->
allowedExtensions.any { name.endsWith(it) }
}
}
dialog.isVisible = true
result = dialog.file?.let { File(dialog.directory, it) }
isOpen = false
onDispose { }
}
}
}
}
```
### Scroll Behavior
```kotlin
@Composable
fun DesktopScrollableColumn(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
Box(modifier) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize()
) {
content()
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd),
adapter = rememberScrollbarAdapter(scrollState)
)
}
}
```
### Keyboard Navigation
```kotlin
@Composable
fun KeyboardNavigableList(
items: List<Note>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
onActivate: (Note) -> Unit
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
LazyColumn(
modifier = Modifier
.focusRequester(focusRequester)
.focusable()
.onKeyEvent { event ->
when {
event.key == Key.DirectionDown && event.type == KeyEventType.KeyDown -> {
onSelect((selectedIndex + 1).coerceAtMost(items.lastIndex))
true
}
event.key == Key.DirectionUp && event.type == KeyEventType.KeyDown -> {
onSelect((selectedIndex - 1).coerceAtLeast(0))
true
}
event.key == Key.Enter && event.type == KeyEventType.KeyDown -> {
items.getOrNull(selectedIndex)?.let { onActivate(it) }
true
}
else -> false
}
}
) {
itemsIndexed(items) { index, note ->
NoteCard(
note = note,
isSelected = index == selectedIndex,
modifier = Modifier.clickable { onSelect(index) }
)
}
}
}
```
### Multi-Window Support
```kotlin
@Composable
fun ApplicationScope.NoteDetailWindow(
note: Note,
onClose: () -> Unit
) {
Window(
onCloseRequest = onClose,
title = "Note by ${note.author.name}",
state = rememberWindowState(width = 600.dp, height = 400.dp)
) {
NoteDetailScreen(note)
}
}
// Usage in main application
var openNotes by remember { mutableStateOf<List<Note>>(emptyList()) }
openNotes.forEach { note ->
key(note.id) {
NoteDetailWindow(
note = note,
onClose = { openNotes = openNotes - note }
)
}
}
```
### Tooltips
```kotlin
@Composable
fun TooltipButton(
tooltip: String,
onClick: () -> Unit,
content: @Composable () -> Unit
) {
TooltipArea(
tooltip = {
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.inverseSurface
) {
Text(
text = tooltip,
modifier = Modifier.padding(8.dp),
color = MaterialTheme.colorScheme.inverseOnSurface
)
}
}
) {
IconButton(onClick = onClick) {
content()
}
}
}
```
## Desktop Layout Pattern
```kotlin
@Composable
fun DesktopAppLayout(
currentScreen: Screen,
onNavigate: (Screen) -> Unit,
content: @Composable () -> Unit
) {
Row(Modifier.fillMaxSize()) {
// Sidebar navigation
NavigationRail(
modifier = Modifier.width(72.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Spacer(Modifier.height(16.dp))
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == Screen.Feed,
onClick = { onNavigate(Screen.Feed) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Email, "Messages") },
label = { Text("DMs") },
selected = currentScreen == Screen.Messages,
onClick = { onNavigate(Screen.Messages) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Notifications, "Notifications") },
label = { Text("Alerts") },
selected = currentScreen == Screen.Notifications,
onClick = { onNavigate(Screen.Notifications) }
)
Spacer(Modifier.weight(1f))
NavigationRailItem(
icon = { Icon(Icons.Default.Settings, "Settings") },
label = { Text("Settings") },
selected = currentScreen == Screen.Settings,
onClick = { onNavigate(Screen.Settings) }
)
}
// Divider
VerticalDivider()
// Main content
Box(Modifier.weight(1f).fillMaxHeight()) {
content()
}
}
}
```
## Build Configuration
```kotlin
// desktopApp/build.gradle.kts
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
}
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(project(":quartz"))
}
compose.desktop {
application {
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
nativeDistributions {
targetFormats(
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb
)
packageName = "Amethyst"
packageVersion = "1.0.0"
macOS {
bundleID = "com.vitorpamplona.amethyst.desktop"
iconFile.set(project.file("icons/icon.icns"))
}
windows {
iconFile.set(project.file("icons/icon.ico"))
menuGroup = "Amethyst"
}
linux {
iconFile.set(project.file("icons/icon.png"))
}
}
}
}
```
+165
View File
@@ -0,0 +1,165 @@
# Quartz KMP Conversion Skill
When working with Quartz library conversion to Kotlin Multiplatform:
## Current Structure (Android-only)
```
quartz/
├── build.gradle
└── src/
├── main/kotlin/ # All Nostr code here
├── test/
└── androidTest/
```
## Target Structure (KMP)
```
quartz/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/ # Shared protocol code
├── commonTest/kotlin/ # Shared tests
├── androidMain/kotlin/ # Android crypto, storage
├── androidTest/kotlin/
├── jvmMain/kotlin/ # Desktop crypto, storage
└── jvmTest/kotlin/
```
## Platform Abstractions Required
### 1. Cryptography (expect/actual)
```kotlin
// commonMain
expect object Secp256k1 {
fun sign(data: ByteArray, privateKey: ByteArray): ByteArray
fun verify(data: ByteArray, signature: ByteArray, pubKey: ByteArray): Boolean
fun pubKeyCreate(privateKey: ByteArray): ByteArray
}
// androidMain - uses secp256k1-kmp-jni-android
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
// jvmMain - uses secp256k1-kmp-jni-jvm
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
```
### 2. NIP-44 Encryption (Sodium)
```kotlin
// commonMain
expect object Nip44 {
fun encrypt(plaintext: String, sharedSecret: ByteArray): String
fun decrypt(ciphertext: String, sharedSecret: ByteArray): String
}
// androidMain - lazysodium-android
// jvmMain - lazysodium-java or libsodium-jni
```
### 3. Secure Random
```kotlin
// commonMain
expect fun secureRandomBytes(size: Int): ByteArray
// androidMain
actual fun secureRandomBytes(size: Int): ByteArray {
return SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
// jvmMain
actual fun secureRandomBytes(size: Int): ByteArray {
return java.security.SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
```
## Build Configuration
```kotlin
// quartz/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
jvm("desktop") {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.collections.immutable)
}
}
val androidMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.android)
implementation(libs.lazysodium.android)
}
}
val desktopMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
// lazysodium-java or alternative
}
}
}
}
android {
namespace = "com.vitorpamplona.quartz"
compileSdk = 35
defaultConfig.minSdk = 26
}
```
## Migration Steps
1. **Convert build.gradle to build.gradle.kts** with KMP plugin
2. **Move pure Kotlin code** to `commonMain/`
3. **Identify platform dependencies** (crypto, JNA, Android APIs)
4. **Create expect declarations** for platform-specific APIs
5. **Implement actuals** in androidMain and jvmMain
6. **Update imports** in amethyst module
7. **Test on both platforms**
## Files to Move to commonMain
Most of Quartz can be shared:
- Event classes and parsing
- Filter definitions
- Relay message types
- NIP implementations (logic only)
- Utilities (hex encoding, bech32)
## Files Needing expect/actual
- `Secp256k1.kt` - Signature operations
- `Nip04.kt` - Legacy encryption (uses AES)
- `Nip44.kt` - Modern encryption (uses ChaCha)
- `KeyPair.kt` - Key generation
+2 -1
View File
@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.diffplugSpotless)
alias(libs.plugins.googleServices) apply false
alias(libs.plugins.jetbrainsComposeCompiler) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
alias(libs.plugins.serialization)
@@ -64,4 +65,4 @@ tasks.register('installGitHook', Copy) {
into { new File(rootProject.rootDir, '.git/hooks') }
filePermissions { unix(0777) }
}
tasks.getByPath(':amethyst:preBuild').dependsOn installGitHook
tasks.getByPath(':amethyst:preBuild').dependsOn installGitHook
+59
View File
@@ -0,0 +1,59 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
alias(libs.plugins.jetbrainsKotlinJvm)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
// Quartz Nostr library (will use JVM target)
implementation(project(":quartz"))
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.swing)
// Networking
implementation(libs.okhttp)
// JSON
implementation(libs.jackson.module.kotlin)
// Collections
implementation(libs.kotlinx.collections.immutable)
}
compose.desktop {
application {
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Amethyst"
packageVersion = "1.0.0"
description = "Nostr client for desktop"
vendor = "Amethyst Contributors"
macOS {
bundleID = "com.vitorpamplona.amethyst.desktop"
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
}
windows {
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
menuGroup = "Amethyst"
upgradeUuid = "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
}
linux {
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
}
}
}
}
@@ -0,0 +1,306 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.desktop
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyShortcut
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
enum class Screen {
Feed,
Search,
Messages,
Notifications,
Profile,
Settings
}
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Amethyst"
) {
MenuBar {
Menu("File") {
Item(
"New Note",
shortcut = KeyShortcut(Key.N, ctrl = true),
onClick = { /* TODO: Open new note dialog */ }
)
Separator()
Item(
"Settings",
shortcut = KeyShortcut(Key.Comma, ctrl = true),
onClick = { /* TODO: Open settings */ }
)
Separator()
Item(
"Quit",
shortcut = KeyShortcut(Key.Q, ctrl = true),
onClick = ::exitApplication
)
}
Menu("Edit") {
Item("Copy", shortcut = KeyShortcut(Key.C, ctrl = true), onClick = { })
Item("Paste", shortcut = KeyShortcut(Key.V, ctrl = true), onClick = { })
}
Menu("View") {
Item("Feed", onClick = { })
Item("Messages", onClick = { })
Item("Notifications", onClick = { })
}
Menu("Help") {
Item("About Amethyst", onClick = { })
Item("Keyboard Shortcuts", onClick = { })
}
}
App()
}
}
@Composable
fun App() {
var currentScreen by remember { mutableStateOf(Screen.Feed) }
MaterialTheme(
colorScheme = darkColorScheme()
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Row(Modifier.fillMaxSize()) {
// Sidebar Navigation
NavigationRail(
modifier = Modifier.width(80.dp).fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Spacer(Modifier.height(16.dp))
NavigationRailItem(
icon = { Icon(Icons.Default.Home, contentDescription = "Feed") },
label = { Text("Feed") },
selected = currentScreen == Screen.Feed,
onClick = { currentScreen = Screen.Feed }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Search, contentDescription = "Search") },
label = { Text("Search") },
selected = currentScreen == Screen.Search,
onClick = { currentScreen = Screen.Search }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Email, contentDescription = "Messages") },
label = { Text("DMs") },
selected = currentScreen == Screen.Messages,
onClick = { currentScreen = Screen.Messages }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Notifications, contentDescription = "Notifications") },
label = { Text("Alerts") },
selected = currentScreen == Screen.Notifications,
onClick = { currentScreen = Screen.Notifications }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Person, contentDescription = "Profile") },
label = { Text("Profile") },
selected = currentScreen == Screen.Profile,
onClick = { currentScreen = Screen.Profile }
)
Spacer(Modifier.weight(1f))
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
NavigationRailItem(
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
label = { Text("Settings") },
selected = currentScreen == Screen.Settings,
onClick = { currentScreen = Screen.Settings }
)
Spacer(Modifier.height(16.dp))
}
VerticalDivider()
// Main Content
Box(
modifier = Modifier.weight(1f).fillMaxHeight().padding(24.dp)
) {
when (currentScreen) {
Screen.Feed -> FeedPlaceholder()
Screen.Search -> SearchPlaceholder()
Screen.Messages -> MessagesPlaceholder()
Screen.Notifications -> NotificationsPlaceholder()
Screen.Profile -> ProfilePlaceholder()
Screen.Settings -> SettingsPlaceholder()
}
}
}
}
}
}
@Composable
fun FeedPlaceholder() {
Column {
Text(
"Feed",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Your Nostr feed will appear here.\n\nConnect to relays and follow users to see notes.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun SearchPlaceholder() {
Column {
Text(
"Search",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Search for users, notes, and hashtags.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun MessagesPlaceholder() {
Column {
Text(
"Messages",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Your encrypted direct messages will appear here.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun NotificationsPlaceholder() {
Column {
Text(
"Notifications",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Mentions, replies, and reactions will appear here.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun ProfilePlaceholder() {
Column {
Text(
"Profile",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Login with your Nostr keys to see your profile.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun SettingsPlaceholder() {
Column {
Text(
"Settings",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
"Configure relays, appearance, and account settings.",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+4 -1
View File
@@ -1,5 +1,6 @@
[versions]
accompanistAdaptive = "0.37.3"
composeMultiplatform = "1.7.1"
activityCompose = "1.12.1"
agp = "8.13.2"
android-compileSdk = "36"
@@ -122,6 +123,7 @@ jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
lazysodium-java = { group = "com.goterl", name = "lazysodium-java", version.ref = "lazysodiumJava" }
@@ -167,4 +169,5 @@ serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" }
stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
+1
View File
@@ -34,3 +34,4 @@ include ':benchmark'
include ':quartz'
include ':commons'
include ':ammolite'
include ':desktopApp'