Files
amethyst/docs/shared-ui-analysis.md
Claude b0c6ffb821 refactor(commons): consolidate package taxonomy + add architecture doc
Document the commons module's purpose, source-set layout, and the CLI-safe vs
UI boundary in commons/ARCHITECTURE.md, then clean up the clearest package
overlaps that had accumulated:

- merge duplicate util/utils -> util (all source sets)
- unify service/services -> service (jvmAndroid)
- move data/UserMetadataCache -> model/cache
- fold compose/ into ui/ (ui/article, editor, elements, layouts, markdown,
  nip53LiveActivities, and Compose helpers in ui/state + ui/text)
- move ProfileBroadcastBanner composable into profile/ui

All changes are whole-file/whole-package moves with import rewrites; no logic
changed. The chess logic/UI split is documented as deferred debt (it needs
file-level surgery, not moves). Marks docs/shared-ui-analysis.md superseded.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 17:03:57 +00:00

6.4 KiB

Shared UI Components Analysis

Superseded (2026-05-30). This was the original feasibility analysis that motivated the commons module (then called "shared-ui"). The module now exists and is documented authoritatively in commons/ARCHITECTURE.md, which defines the package taxonomy, the CLI-safe / UI boundary, and the "where does my code go?" guide. Keep this file only as historical context.

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