- Add DesktopHttpClient and DesktopRelayConnectionManager for relay WebSocket connections - Implement AccountManager with key generation and nsec/npub login support - Create LoginScreen with key import and new key generation UI - Build FeedScreen with live global feed subscription and note cards - Add ProfileScreen showing account info and logout - Update RelaySettingsScreen with connection status and relay management - Configure JDK 21 toolchain for desktop builds - Add shared UI analysis documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.1 KiB
Shared UI Components Analysis
Analysis of Android vs Desktop UI to identify reusable Compose Multiplatform components.
Executive Summary
| Category | Android Files | Shareable As-Is | Needs Abstraction |
|---|---|---|---|
| Formatters | 4 | 2 | 2 |
| Theme | 4 | 4 | 0 |
| Note Components | 35 | ~10 | ~25 |
| Total Potential | ~450+ | ~100+ | ~200+ |
Immediately Shareable Components
These have zero Android dependencies and can be moved to a shared module today:
Formatters (Pure Kotlin)
amethyst/ui/note/PubKeyFormatter.kt -> shared/ui/formatters/
amethyst/ui/note/ZapFormatter.kt -> shared/ui/formatters/
amethyst/ui/note/ZapFormatterNoDecimals.kt -> shared/ui/formatters/
Theme (Pure Compose)
amethyst/ui/theme/Color.kt -> shared/ui/theme/
amethyst/ui/theme/Shape.kt -> shared/ui/theme/
amethyst/ui/theme/Type.kt -> shared/ui/theme/
Layout Primitives
amethyst/ui/theme/ButtonBorder (padding values)
amethyst/ui/theme/Size* (dimension constants)
Components Requiring Abstraction
1. String Resources (R.string.*)
Problem: Android uses R.string.xxx for localized strings.
Solution: Create StringProvider interface:
// commonMain
interface StringProvider {
fun get(key: String): String
fun get(key: String, vararg args: Any): String
}
// androidMain
class AndroidStringProvider(private val context: Context) : StringProvider {
override fun get(key: String) = context.getString(keyToId(key))
}
// jvmMain
class DesktopStringProvider : StringProvider {
private val strings = mapOf(
"post_not_found" to "Post not found",
"now" to "now",
// ...
)
override fun get(key: String) = strings[key] ?: key
}
Affected files:
- TimeAgoFormatter.kt
- BlankNote.kt
- All UI components with text
2. AccountViewModel
Problem: Most UI components depend on AccountViewModel for:
- User profile data
- Settings (image loading, NSFW, etc.)
- Actions (follow, mute, report)
Solution: Create interface in shared module:
// commonMain
interface IAccountState {
val userPubKey: String
val userNpub: String
val settings: IUserSettings
}
interface IUserSettings {
val showImages: Boolean
val showSensitiveContent: Boolean
}
3. Navigation (INav)
Problem: Android uses custom INav interface tied to Compose Navigation.
Solution: Already abstracted! INav is an interface - just needs to be in shared module.
4. Image Loading
Problem: Uses Coil with Android-specific caching.
Solution: Coil 3.x supports Compose Multiplatform. Create shared wrapper:
// commonMain
@Composable
expect fun AsyncImage(
url: String,
contentDescription: String?,
modifier: Modifier,
)
Recommended Shared Module Structure
shared-ui/
├── src/
│ ├── commonMain/kotlin/
│ │ ├── formatters/
│ │ │ ├── PubKeyFormatter.kt
│ │ │ ├── ZapFormatter.kt
│ │ │ └── TimeAgoFormatter.kt (abstracted)
│ │ ├── theme/
│ │ │ ├── Color.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Type.kt
│ │ │ └── Dimensions.kt
│ │ ├── components/
│ │ │ ├── NoteCard.kt
│ │ │ ├── UserAvatar.kt
│ │ │ └── LoadingIndicator.kt
│ │ └── providers/
│ │ ├── StringProvider.kt
│ │ └── ImageProvider.kt
│ ├── androidMain/kotlin/
│ │ └── providers/
│ │ └── AndroidStringProvider.kt
│ └── jvmMain/kotlin/
│ └── providers/
│ └── DesktopStringProvider.kt
└── build.gradle.kts
Implementation Priority
Phase 1: Low-Hanging Fruit (1-2 days)
- Create
shared-uimodule - Move pure Kotlin formatters (PubKeyFormatter, ZapFormatter)
- Move theme colors and dimensions
- Update both apps to use shared module
Phase 2: String Abstraction (2-3 days)
- Create StringProvider interface
- Implement for Android and Desktop
- Migrate TimeAgoFormatter
- Create composable wrappers for common strings
Phase 3: Core UI Components (1 week)
- Abstract AccountViewModel to IAccountState
- Move simple components (BlankNote, LoadingIndicator)
- Create NoteCard shared component
- Integrate with both apps
Phase 4: Complex Components (2+ weeks)
- Image loading abstraction
- RichTextViewer
- ReactionsRow
- Full NoteCompose migration
Desktop vs Android Comparison
| Feature | Android | Desktop | Share Strategy |
|---|---|---|---|
| Note display | NoteCompose.kt (51KB) | NoteCard in FeedScreen | Extract common layout |
| Time formatting | TimeAgoFormatter.kt | SimpleDateFormat inline | Abstract with interface |
| Key display | PubKeyFormatter.kt | Manual truncation | Use shared formatter |
| Colors | Color.kt | darkColorScheme() | Share Color.kt |
| Navigation | INav + Compose Nav | enum Screen | Abstract navigation |
| Images | Coil + OkHttp | Not implemented | Coil Multiplatform |
Current Desktop Implementation Gaps
Desktop currently has basic implementations that could benefit from sharing:
- NoteCard - Has inline date formatting, could use TimeAgoFormatter
- Key display - Manual truncation, should use PubKeyFormatter
- Theme - Uses default darkColorScheme, could use shared theme
- No image loading - Needs Coil integration
Quick Wins
Immediate improvements to desktop using patterns from Android:
// Instead of:
val npub = event.pubKey.take(16) + "..."
// Use shared formatter:
val npub = event.pubKey.toDisplayHexKey()
// Instead of:
val format = SimpleDateFormat("MMM d, HH:mm", Locale.getDefault())
// Use shared formatter:
val timeAgo = timeAgoNoDot(event.createdAt, stringProvider)
Next Steps
- Create shared-ui module in project structure
- Move formatters as proof of concept
- Create StringProvider abstraction
- Iterate on more complex components