Files
amethyst/docs/shared-ui-analysis.md
nrobi144 8c8a4ab9e8 feat: Add relay connections, login, and feed view to desktop app
- 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>
2025-12-26 14:02:44 +02:00

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,
)
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)

  1. Create shared-ui module
  2. Move pure Kotlin formatters (PubKeyFormatter, ZapFormatter)
  3. Move theme colors and dimensions
  4. Update both apps to use shared module

Phase 2: String Abstraction (2-3 days)

  1. Create StringProvider interface
  2. Implement for Android and Desktop
  3. Migrate TimeAgoFormatter
  4. Create composable wrappers for common strings

Phase 3: Core UI Components (1 week)

  1. Abstract AccountViewModel to IAccountState
  2. Move simple components (BlankNote, LoadingIndicator)
  3. Create NoteCard shared component
  4. Integrate with both apps

Phase 4: Complex Components (2+ weeks)

  1. Image loading abstraction
  2. RichTextViewer
  3. ReactionsRow
  4. 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:

  1. NoteCard - Has inline date formatting, could use TimeAgoFormatter
  2. Key display - Manual truncation, should use PubKeyFormatter
  3. Theme - Uses default darkColorScheme, could use shared theme
  4. 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

  1. Create shared-ui module in project structure
  2. Move formatters as proof of concept
  3. Create StringProvider abstraction
  4. Iterate on more complex components