diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 886a0dfe3e..e43dd865f0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -11,9 +11,11 @@ contain implementations of Nostr specifications and utilities to help implement `concord/` (`cordXX`), `buzz/`, plus the binding-agnostic RFC 9420 engine in `mls/`. A new protocol over Nostr belongs here as a package, not as a Gradle module; `quic`/`nestsClient`/`marmotQuic` are modules because they are transports with no Nostr in them. Commons stores -shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). The Desktop -App is designed to be mouse first and so uses a completely different screen and navigation -architecture while sharing the back end components with the android counterpart. `cli` ships `amy`, +shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). Android now +also ships on laptops, so the direction is **one UI**: every screen and the navigation shell at every +window size move to `commonsUI`, `amethyst` shrinks to an Android shim, and a new `desktopApp` +becomes a JVM shim that renders the same UI (see `commons/plans/2026-09-27-one-ui-android-desktop.md`). +Until it lands, today's `desktopApp` still has its own mouse-first screens and navigation. `cli` ships `amy`, a non-interactive JVM command-line client that drives the same `quartz` + `commons` code — used by humans, agents, and interop tests. `quic` is a from-scratch pure-Kotlin QUIC v1 + HTTP/3 + WebTransport client (no JNI, no BouncyCastle), built because no Android-compatible Java QUIC library @@ -105,7 +107,9 @@ amethyst/ codecs. Not WebTransport — the binding has its own ALPNs and writes frames straight onto QUIC streams, so it deliberately does not reuse `nestsClient`'s `WebTransportSession`. -- `amethyst/` & `desktopApp/` = Platform-native layouts and navigation +- `amethyst/` & `desktopApp/` = Platform shims: process/window entry points, services, system + integrations and the actuals of shared ports. Screens and navigation are shared UI and are + moving to `commonsUI` (today's `desktopApp` screens are legacy, to be replaced). - `cli/` = Thin assembly layer over `quartz/` + `commons/` (no new logic allowed). May also depend on `:geode` (for `amy serve`, which embeds the standalone relay); never on `:commonsUI`, `:amethyst` or `:desktopApp`. @@ -207,10 +211,20 @@ etc. instead of re-implementing them. (StateFlow/SharedFlow), so they belong in `commons`; anything that imports `androidx.compose.ui`/`foundation`/`material3`, Coil, or `Res` belongs in `commonsUI`. -- **Keep native** → screen composables/scaffolding (Desktop `Window` vs Android - `Activity`), navigation (sidebar vs bottom nav), platform interactions - (gestures, keyboard shortcuts), system integrations (notifications, file - pickers). + **Screens and navigation are shared too**: screen composables, the nav host, + and the navigation chrome for every window size (bottom bar, rail, permanent + drawer) belong in `commonsUI`, because Android runs on laptops and the new + Desktop app renders the same UI. Adapt to the window with `ScreenLayoutSpec` + / `LocalScreenLayout`, not with a per-platform screen. +- **Keep native** → only what is the platform itself: the process/window entry + point (Android `Activity`/`Service`, Desktop `Window`/tray/menu bar), + system integrations (notifications, file pickers, share sheets, camera, + media3, WebView, keyring/Keystore), and the platform `actual`s or port + implementations the shared UI calls. A new screen goes in `commonsUI` when + its dependencies allow. While `AccountViewModel` is still app-side, one that + needs it may live in `amethyst/`, but keep Android APIs out of it (behind a + port or slot) so it can move later. Don't add screens to today's `desktopApp` + that the shared UI will have to re-create. When extracting a composable: move it to `commonsUI/commonMain/` (see `/compose-expert`), add expect/actual for any platform behavior (see @@ -429,8 +443,11 @@ Do this before considering the task complete. at byte level (`perl -CSD -pe 's/\x{202E}/\\u202E/g'`). ### Navigation Shell -- **Desktop**: Sidebar + main content area -- **Android**: Bottom navigation +One shell, picked by window size rather than by platform: `ScreenLayoutSpec` +chooses the bottom bar (compact), the rail, or the permanent drawer (wide, +landscape, tall enough), and docks the notification panel on very wide windows. +It is moving to `commonsUI` with `AppNavigation`. Today's `desktopApp` still has +its own sidebar shell, which will be replaced. ## Git Workflow diff --git a/.claude/skills/compose-expert/SKILL.md b/.claude/skills/compose-expert/SKILL.md index e6eced37b1..c5c6afdc30 100644 --- a/.claude/skills/compose-expert/SKILL.md +++ b/.claude/skills/compose-expert/SKILL.md @@ -1,6 +1,6 @@ --- 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). +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 platform entry points (Activity, Window, tray) to android-expert/desktop-expert; the navigation shell itself is shared UI. Complements kotlin-expert (handles Kotlin language aspects of state/annotations). --- # Compose Multiplatform Expert @@ -34,12 +34,17 @@ Visual UI patterns for sharing composables across Android and Desktop. - **Theme utilities**: Color calculations, style helpers - **Material3 components**: Any UI using Material primitives +**Screens and navigation are shared too.** Android also ships on laptops and the new +Desktop app renders the same UI, so screens, the nav host and the bottom bar / rail / +permanent drawer all belong in `commonsUI`. They adapt to the window with +`ScreenLayoutSpec` / `LocalScreenLayout`, not per platform. See +`commons/plans/2026-09-27-one-ui-android-desktop.md`. + ### 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 +- **Entry points**: Android `Activity`/`Service`, Desktop `Window`, tray, menu bar +- **System integrations**: File pickers, notifications, share sheets, camera, media3, WebView +- **Platform leaves inside shared UI**: behind expect/actual or a slot the shim fills ### Decision Framework @@ -563,12 +568,14 @@ fun FeedList(items: List) { 4. Add caching if generated dynamically 5. Wrap in @Composable for easy use -### Navigation (Delegate) +### Navigation -For navigation patterns: -- Android bottom nav → `android-expert` -- Desktop sidebar → `desktop-expert` -- Multi-window → `desktop-expert` +The navigation shell is shared (`ScreenLayoutSpec` picks bottom bar, rail or permanent +drawer by window size) and is moving to `commonsUI` with `AppNavigation`. Delegate only +the platform leaves: +- Activity / intent plumbing → `android-expert` +- Window, tray, menu bar, multi-window → `desktop-expert` +- Today's legacy `desktopApp` sidebar shell → `desktop-expert` (to be replaced) ## Related Skills diff --git a/.claude/skills/desktop-expert/SKILL.md b/.claude/skills/desktop-expert/SKILL.md index 477880202d..b9def76159 100644 --- a/.claude/skills/desktop-expert/SKILL.md +++ b/.claude/skills/desktop-expert/SKILL.md @@ -248,9 +248,16 @@ application { ## 5. Desktop Navigation Patterns -### NavigationRail (Current Pattern) +### NavigationRail (Current Pattern — legacy) -Desktop uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation. +> **Being replaced.** Android now also ships on laptops, and the plan is one UI: the +> shared navigation shell (bottom bar / rail / permanent drawer picked by window size +> via `ScreenLayoutSpec`) moves to `commonsUI`, and a new `desktopApp` becomes a JVM +> shim that renders it. Don't grow this sidebar shell or add Desktop-only screens; see +> `commons/plans/2026-09-27-one-ui-android-desktop.md`. What stays Desktop-specific is +> the window, tray, menu bar, keyboard shortcuts and file system integration. + +Today's desktop app uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation. ```kotlin Row(Modifier.fillMaxSize()) { diff --git a/.claude/skills/kotlin-multiplatform/SKILL.md b/.claude/skills/kotlin-multiplatform/SKILL.md index bd21f16742..87e6172025 100644 --- a/.claude/skills/kotlin-multiplatform/SKILL.md +++ b/.claude/skills/kotlin-multiplatform/SKILL.md @@ -52,8 +52,9 @@ Q: Does it vary by platform or by JVM vs non-JVM? │ Example: Jackson JSON parsing (JVM library) │ └─ Complex/UI-related - → Keep platform-specific - Example: Navigation (Activity vs Window too different) + → Share it in commonsUI; expect/actual or a slot only for the platform leaf + Example: Navigation. One shell in commonsUI, picked by window size + (ScreenLayoutSpec); only the Activity / Window entry point is native Final check: Q: Maintenance cost of abstraction < duplication cost? @@ -85,10 +86,13 @@ val jvmAndroid = create("jvmAndroid") { ``` **Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web. -**Navigation → platform-specific:** -- Android: `MainActivity` (Activity + Compose Navigation) -- Desktop: `Window` + sidebar + MenuBar -**Why:** UI paradigms fundamentally different. +**Navigation → shared, entry points → platform-specific:** +- Shared (`commonsUI`, moving there): the nav host, bottom bar / rail / + permanent drawer, chosen by window size via `ScreenLayoutSpec` +- Android: `MainActivity` hosts it; Desktop: a `Window` (+ MenuBar, tray) hosts it +**Why:** Android runs on laptops too, and the new Desktop app renders the same +UI. See `commons/plans/2026-09-27-one-ui-android-desktop.md`. Today's +`desktopApp` sidebar shell is legacy. ## Mental Model: Source Sets as Dependency Graph @@ -183,15 +187,15 @@ Quick decision guidelines based on codebase patterns: ### Sometimes Abstract - **Business logic:** YES - state machines, data processing - **ViewModels:** YES - state + business logic shareable (StateFlow/SharedFlow) -- **Screen layouts:** NO - platform-native (Window vs Activity) -- **Why:** ViewModels contain platform-agnostic state; Screens render differently per platform +- **Screen layouts:** YES - shared in `commonsUI`; they adapt to window size, not platform +- **Why:** One UI for Android (phones to laptops) and Desktop; only entry points differ ### Rarely Abstract - **Complex UI components** (composables with heavy platform dependencies) - **Why:** Platform paradigms can differ significantly ### Never Abstract -- **Navigation** (Activity vs Window fundamentally different) +- **Process / window entry points** (Activity, Service, Window, tray) - **Permissions** (Android vs iOS APIs incompatible) - **Platform UX patterns** - **Why:** Too platform-specific, abstraction creates leaky APIs @@ -203,7 +207,7 @@ Quick decision guidelines based on codebase patterns: | PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs | | TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings | | ViewModels (state + logic) | ✅ YES | StateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible | -| Screen layouts (Scaffold, nav) | ❌ NO | Window vs Activity, sidebar vs bottom nav fundamentally different | +| Screen layouts (Scaffold, nav) | ✅ YES (moving) | One UI; `ScreenLayoutSpec` picks bottom bar / rail / drawer by window size | | Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper | ## expect/actual Mechanics @@ -333,8 +337,9 @@ fun validateSignature(...) { ... } // Duplicated! // ❌ BAD expect fun NavigationComponent(...) ``` -**Why:** Navigation paradigms too different (Activity vs Window) -**Fix:** Keep platform-specific, accept duplication +**Why:** It forks the whole navigation UI per platform. +**Fix:** Write the navigation once in `commonsUI`; put only the platform leaf +(an Activity result, a file picker) behind expect/actual or a slot ### 2. Under-Sharing **Problem:** Duplicating business logic across platforms @@ -384,8 +389,8 @@ import com.fasterxml.jackson.databind.ObjectMapper | **ViewModels** | **commons/commonMain/viewmodels/** | **StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible** | | UI formatters (pure) | commons/commonMain | Reusable, no dependencies | | UI components (simple) | commonsUI/commonMain | Cards, buttons, dialogs (Compose UI never goes in `commons`) | -| **Screen layouts** | **Platform-specific** | **Window vs Activity, sidebar vs bottom nav** | -| Navigation | Platform-specific only | Activity vs Window too different | +| **Screen layouts** | **commonsUI** | **One UI; adapt to window size with `ScreenLayoutSpec`** | +| Navigation | commonsUI (entry point native) | Nav host + chrome shared; Activity / Window hosts it | | Permissions | Platform-specific only | APIs incompatible | | Platform UX (menus, etc.) | Platform-specific only | Native feel required | diff --git a/commons/ARCHITECTURE.md b/commons/ARCHITECTURE.md index 7fd1105b31..1606e47300 100644 --- a/commons/ARCHITECTURE.md +++ b/commons/ARCHITECTURE.md @@ -4,8 +4,8 @@ | Consumer | Kind | Uses from `commons` | |----------------|------------------------------|----------------------------------------------| -| `amethyst` | Android app (touch-first) | everything (models, state, ViewModels) + `commonsUI` | -| `desktopApp` | Desktop JVM app (mouse-first)| everything (models, state, ViewModels) + `commonsUI` | +| `amethyst` | Android app (phones, tablets, laptops) | everything (models, state, ViewModels) + `commonsUI` | +| `desktopApp` | Desktop JVM app | everything (models, state, ViewModels) + `commonsUI`; today's mouse-first screens are to be replaced by the shared UI | | `cli` (`amy`) | Headless JVM CLI (no UI) | everything — `commons` is headless by construction; it never sees `commonsUI` | | `nappletHost` | Android WebView sandbox | napplet contract + `commonsUI` (for the shell/shim Compose resources) | | iOS (future) | iOS app | everything + `commonsUI`; expected to share most UI with Android | @@ -15,17 +15,21 @@ - **`quartz/`** — Nostr protocol: events, NIPs, crypto, relay framing. No app state, no UI, no caches of "what this user follows." -- **`commons/`** — everything an Amethyst *client* needs that isn't a - platform-native screen, navigation shell, **or Compose UI**: domain models +- **`commons/`** — everything an Amethyst *client* needs that isn't + platform-specific **or Compose UI**: domain models (`Note`, `User`), in-memory state holders, ViewModels, the relay-subscription client, shared business services. - **`commonsUI/`** — the Compose UI components that more than one front end renders, plus everything only they need (icons, theme, Coil fetchers, markdown, the `composeResources` strings/fonts and the generated `Res`). Depends on `commons` as `api`. See `commonsUI/ARCHITECTURE.md`. -- **`amethyst/` & `desktopApp/`** — platform-native screens, navigation - (bottom-nav vs sidebar), gestures, system integration. They assemble - `commons` pieces; they should not re-implement them. +- **`amethyst/` & `desktopApp/`** — platform shims: the Activity / Window + entry points, services, system integration, and the platform implementations + of shared ports. They assemble `commons` pieces; they should not + re-implement them. **Screens and navigation are being moved into + `commonsUI`** so Android (which now also runs on laptops) and a new JVM + Desktop app render one UI; see + [`plans/2026-09-27-one-ui-android-desktop.md`](plans/2026-09-27-one-ui-android-desktop.md). > The goal is **"write it once in `commons`"**. Before adding a manager, cache, > filter, ViewModel, or composable to an app module, check whether it already @@ -223,9 +227,10 @@ compiles: `commonMain` → `jvmAndroid` → platform-specific. See ### Where does my code go? (quick guide) 1. **Pure Nostr protocol** (events/NIPs/crypto)? → not here, it's `quartz`. -2. **A composable** rendered by ≥2 front ends, or that you want iOS to share? → - `commonsUI`, in `ui/` or `/ui` (same package tree as here). - Also anything that imports Coil, `Res`, or a `foundation`/`ui` state type. +2. **A composable** (screens and navigation chrome included)? → `commonsUI`, in + `ui/` or `/ui` (same package tree as here). Also anything that + imports Coil, `Res`, or a `foundation`/`ui` state type. Only the platform + entry points and system integrations stay in `amethyst/`/`desktopApp/`. 3. **A ViewModel / `StateFlow` state holder**? → `viewmodels` or `state` (or `` if feature-scoped). Keep it CLI-safe where practical. 4. **Relay subscription / filter assembly**? → `relayClient`. diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index e5ee522cd6..bcf728c8b9 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1,5 +1,14 @@ # Full sweep: `amethyst/` → `:commons` migration candidates +> **Direction change (2026-09-27): one UI.** Android now ships on laptops, so every +> screen and the navigation shell move to `commonsUI`, `amethyst` becomes an Android +> shim, and a new JVM `desktopApp` replaces the current one with the same UI. See +> [2026-09-27-one-ui-android-desktop.md](2026-09-27-one-ui-android-desktop.md). That +> supersedes, below: the STAY list's screens/navigation entries, "decompose `Account`, +> shrink `AccountViewModel`" (both now **move**), Wave 2 part B (dropped), and the +> "Desktop phase" merges of Desktop's own forks (dropped). This file stays the log of +> what moved; Wave 4 is planned and measured in the new plan. + > **Execution status (updated 2026-08-30, same branch):** Waves 0-1 are DONE > on this branch — the 12 shim deletions, the 38-file relayClient batch > (with `AccountScopedQuery` generalized to `IAccount`), the okhttp stack → @@ -297,10 +306,15 @@ formatters). ## STAY (correctly platform-native) -- **Navigation shell & screens**: `*Screen.kt`, `*TopBar.kt`, `New*Button.kt`, +> **Revised 2026-09-27.** The navigation shell and the screens no longer stay: they move +> to `commonsUI` (see the one-UI plan). What is listed below is what remains +> platform-native. The media, capture and service entries stay as *implementations*; +> the screens that render them reach them through ports or slots. + +- ~~**Navigation shell & screens**: `*Screen.kt`, `*TopBar.kt`, `New*Button.kt`, `INav`/`Route`/`RouteMaker`/`AppNavigation`, drawer/bottom-bar, `AccountScreen`/`AccountSessionManager`/`LoggedInPage`, `loggedOff/`, - `settings/` screens (~480 files import INav/Route — by design). + `settings/` screens.~~ Moving to `commonsUI` (`RouteMaker` to `commons`). - **Process/DI roots**: `Amethyst.kt`, `AppModules.kt`, `EncryptedStorage`, `LocalPreferences`, `DebugUtils`, `model/accountsCache`, `model/preferences/` (the one genuinely-Android model package: DataStore/ @@ -807,7 +821,11 @@ Options, for the maintainer to pick: `ui/theme/Shape.kt` into `commons/ui/theme/Sizes.kt`, plus `painterRes`/`TimeAgo`/`NewItemsBubble` decisions — the audit's "strings-only" tally under-counted transitive deps. -- **Wave 4: Account/AccountViewModel decomposition** — long-tail. +- **Wave 4: Account/AccountViewModel** — **re-scoped 2026-09-27** from + "decomposition" to **moving both to `commons`**. Measured and sequenced in + [2026-09-27-one-ui-android-desktop.md](2026-09-27-one-ui-android-desktop.md): the + `Account` group is 77 files / ~23.9k lines inside `model/`, with 5 hard-blocked files + and 14 exit edges to cut. ### Environment notes for the next session (hard-won) @@ -924,6 +942,10 @@ and `RailCapability` use it, `OnchainZapResolver` does not. ### Wave 2 part B — compatibility analysis and the road to deleting `DesktopLocalCache` (2026-09-19) +> **Dropped 2026-09-27.** The new desktop app uses `LocalCache` and `Account` directly; +> the current `desktopApp` keeps its fork until it is retired. The analysis below is +> kept for the record. + Part A moved the cache. Part B is retiring the Desktop fork. The naive framing ("repoint ~70 consumers and delete 1,177 lines") is wrong; what follows is the measured picture. diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md new file mode 100644 index 0000000000..f844cd7fa7 --- /dev/null +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -0,0 +1,216 @@ +--- +title: "One UI: the whole app moves to commonsUI, Android and Desktop become shims" +type: refactor +status: in-progress +date: 2026-09-27 +owner: commons +consumers: amethyst, desktopApp, commonsUI +--- + +# One UI for Android and Desktop + +## The decision (maintainer, 2026-09-27) + +Android now ships on laptops, so Amethyst Android needs a desktop-class UI. Rather than +maintain two desktop UIs, there will be one: + +- **`commonsUI` holds the whole app UI.** Every screen, the navigation host, and the + navigation chrome at every size (bottom bar, rail, permanent drawer, docked panels). +- **`amethyst/` becomes an Android shim**: Activities, services, notifications, media3, + camera, WebView, Health Connect, Keystore/DataStore actuals, flavours. Nothing that is not + Android itself. +- **A new `desktopApp` becomes a JVM shim** that runs the same `commonsUI` app, so Desktop + looks exactly like Android on a laptop: Window, tray, menu bar, keyring, file pickers. + It **replaces** the current `desktopApp`, which keeps shipping until the new one reaches + parity, then goes. + +This supersedes the "screens and navigation stay platform-native" rule that +`.claude/CLAUDE.md`, `commons/ARCHITECTURE.md`, the `kotlin-multiplatform` / +`compose-expert` / `desktop-expert` skills and the sweep tracker's STAY list used to state. +Those were updated alongside this plan. + +## What this changes in the running migration + +The tracker ([2026-08-30-commons-migration-sweep.md](2026-08-30-commons-migration-sweep.md)) +stays the log of what moved. These of its conclusions no longer hold: + +| Old conclusion | Now | +|---|---| +| `Account` is a god object: "decompose, don't move". `AccountViewModel`: "shrink, don't move". | **Move both** to `commons`. They were only to be decomposed because Desktop was going to keep its own; the shared screens now need the same `Account` on both platforms. Narrow interfaces can still be carved out later, for tests and the CLI, but they are not a prerequisite. | +| STAY: `*Screen.kt`, `*TopBar.kt`, `AppNavigation`, `INav`/`Route`/`RouteMaker`, drawer/bottom bar, `LoggedInPage`, `loggedOff/`, `settings/` screens | **All move** to `commonsUI` (`RouteMaker` to `commons`, it has no Compose). | +| Wave 2 part B: turn `DesktopLocalCache` into a facade over `EventCache` | **Dropped.** The new desktop app uses `LocalCache` and `Account` directly. The old `desktopApp` keeps its fork until it is retired. | +| "Desktop phase": merge Desktop's `ToggleableTimeAgoText`, `TimeAgoFormatter`, `ChatBubbleLayout` forks onto the shared ones | **Dropped** for the same reason. | +| `jdk.localedata` in Desktop's packaged runtime (+~28 MB) is "a packaging call" | **Required** by the new desktop app: the shared date formatters need the CLDR data. | + +## Where we start from + +The Android app already contains the laptop UI. `ScreenLayoutSpec` (now in +`commonsUI/…/ui/layouts/ScreenLayout.kt`) picks one of three navigation tiers from the window +size alone (bottom bar below 600dp, rail, permanent drawer when wide, landscape and at least +600dp tall), docks the notification panel from 1200dp, and caps every destination to a 600dp +reading column (`CappedScreenContent`). A JVM window can feed it `widthDp`/`heightDp` and +get the same answer. Still app-side and part of the move: `AppNavigation.kt` (1,370 lines), +`AppNavigationRail.kt`, the permanent drawer in `AccountSwitcherAndLeftDrawerLayout.kt`, +`MessagesTwoPane.kt`, and `MainActivity.kt` (493 lines, the part that is not Activity +plumbing). + +## Prerequisites the move surfaced + +- **Navigation library.** The app uses `androidx.navigation:navigation-compose` 2.10.1. Its + Gradle module metadata publishes an `androidJvm` variant and only **`jvmStubs`** for `jvm` + (checked 2026-09-27), so it cannot run the nav host on Desktop. The multiplatform path is + JetBrains' `org.jetbrains.androidx.navigation:navigation-compose`, which resolves to the + androidx artifact on Android. That swap (and its licence check, per CLAUDE.md) comes before + `AppNavigation` can move. +- **The app root.** `Amethyst.instance` (the `AppModules` graph) is read by 178 files, 115 of + them under `ui/`. Shared screens cannot reach an Android `Application`. The root needs a + commons-side interface for what screens read from it, provided once at the composition root + (as `LocalUserFinderAccount` / `LocalEventFinder` already are), with an Android and a JVM + implementation. +- **Android-only libraries rendered inside screens.** Each needs an expect/actual or a slot the + shim fills: media3 (46 files), Vico charts (8), WebView (5), CameraX (4), Health Connect (4), + ML Kit (3, `play` flavour only). +- **Current-Desktop-only features.** The current `desktopApp` (260 files, ~70k lines) has + features Android lacks: deck columns, the article editor, highlights, scheduled-post + screens, keyboard shortcuts, menu bar, tray. Each needs a call before the old app is + retired: bring it into `commonsUI` for both platforms, or keep it in the new JVM shim. That + inventory is not done yet. + +## Wave 4, measured: what moves with `Account` + +Measured 2026-09-27 on `main` @ `c13e496c` with a closure script (not committed; the method +is below) over `amethyst/src/{main,play,fdroid}`. + +**An unconstrained closure is useless.** Following every edge from `Account.kt` reaches +1,724 of the app's 1,762 files, because a few edges leave `model/` for the app root +(`Amethyst.kt`), `ui/navigation` and `ui/screen`, and those reach everything. The useful +measure is the group reachable **inside `model/`**, plus the list of edges that leave it. +Each leaving edge is a seam to cut before the group can move. + +### The group + +- **75 files, 22,753 lines**: `Account.kt`, `AccountSettings.kt`, the `Account*Actions` files, + `EventBroadcaster`, and 60-odd per-feature state holders (`nip51Lists/*`, `nip65RelayList`, + `serverList/*`, `topNavFeeds/*`, `nip46Signer/*`, `cordn/*`, …). That is 75 of the 89 files + in `model/`. +- **Adding `EventProcessor`** (`ui/screen/loggedIn/DecryptAndIndexProcessor.kt`, which + `Account` constructs) brings it to 77 files, 23,850 lines, and adds no new exit edge except + two `Amethyst.instance.notificationDispatcher` calls. +- **11 files use `java.*`** (`BigDecimal`, `ConcurrentHashMap`, `UUID`, `Base64`, `File`, + `Locale`, and `OkHttpClient` in `CashuWalletState`). So the group lands in + **`commons/jvmAndroid`** first and promotes to `commonMain` later, the same route + `LocalCache` took. +- **No Compose UI** in any of them. + +### Hard blockers inside the group (5 files) + +| File | Blocker | Proposed cut | +|---|---|---| +| `Account.kt` | `BuildConfig.VERSION_NAME` (donation prompt, 3 sites) | constructor parameter `appVersion: String` | +| `AccountSyncedSettingsInternal.kt` | `Resources.getSystem()` + `ConfigurationCompat` for the system language list; `DefaultBottomBarEntries` from `ui/navigation/bottombars/NavBarItem.kt` | languages: an injected `() -> List` or a small expect/actual; defaults: move the default entry list to `commons/model/navigation`, beside `BottomBarEntry` | +| `GeohashChatIdentityState.kt` | `androidx.core.content.edit`, `LegacySharedPreferences`, `LocalPreferences.LEGACY_WRITES_RETIRED`, `Amethyst.instance.encryptedStorage` | the legacy-prefs read/write is a migration path; put it behind a `GeohashIdentityLegacyStore` port, implemented in the app | +| `AccountZapActions.kt` | `onError: (StringResource, String?)` with `Res.string.bolt12_*` (compose resources, which `commons` cannot see) | a typed error (sealed class) that the UI maps to a string | +| `nip46Signer/Nip46ConsentBridge.kt` | `Res` + `loadStringRes`; `Amethyst.instance.appContext`; the app's `SignerConnectCoordinator` / `SignerConsentCoordinator` / napplet op labels | it is the Android consent-dialog bridge: leave it in the app and inject it into `Account` through an interface | + +### Edges that leave the group (14 targets) + +| Target | Used for | Proposed cut | +|---|---|---| +| `Amethyst.kt` | `keyCache` (Account), `encryptedStorage` (Geohash), `appContext` (Nip46 bridge), `notificationDispatcher` (EventProcessor) | constructor parameters / ports; the notification dispatcher gets an interface | +| `LocalPreferences.kt` | `saveToEncryptedStorage(accountSettings)` on settings change | a `AccountSettingsPersister` port, implemented in the app | +| `DebugUtils.kt` | `logTime` (2 sites) | move `logTime` to `commons/util` (it is timing + `Log`) | +| `service/MainThreadChecker.kt` | `checkNotInMainThread` (HiddenUsersState) | the same settable hook `LocalCache` already needed | +| `service/location/LocationState.kt` | the `LocationResult` type in `geolocationFlow` and the around-me feed | move the result type to `commons`; the `LocationManager` half stays | +| `service/uploads/FileHeader.kt` | the `FileHeader` data type in three send methods | split: the data class to `commons`, the `MediaMetadataRetriever` reader stays | +| `service/relayClient/…/BuzzMembershipEoseManager.kt` | the `MembershipNotificationKinds` constant | move the constant to `commons/model/buzz` | +| `ui/navigation/bottombars/NavBarItem.kt` | `DefaultBottomBarEntries` | see the table above | +| `ui/screen/loggedIn/DecryptAndIndexProcessor.kt` | `EventProcessor`, built by `Account` | moves with the group (see above) | +| `AccountSecretsStore.kt`, `LegacySharedPreferences.kt` | Geohash identity storage | behind the Geohash port above | +| `connectedApps/consent/*Coordinator.kt`, `napplet/NostrSignerOpLabels.kt` | Nip46 consent bridge | stay in the app with the bridge | + +About twenty small cuts, most of them "pass it in" or "move one declaration". None is a +redesign. + +## Wave 4, measured: `AccountViewModel` + +`AccountViewModel.kt` is 3,303 lines. + +- **Android imports.** Its Android imports are `Context` (3 methods take one), `Toast`, + `Uri`, `Handler`/`Looper`, `android.util.LruCache` (5 sites), `NotificationManager` and + `ContextCompat`. It has no `R` references left. +- **`Amethyst.instance`.** It reads the app root in six places: `websocketBuilder`, + `relayStats`, `powPublishQueue`, `localBlossomCacheProbe`, `blossomResolver` and + `appContext`. +- **Other app files.** Outside the `Account` group it depends on 29 app files. They fall into + three kinds: + - **Headless, should move with it:** `RelaySubscriptionsCoordinator`, `ClinkDebitPayer`, + `CallSessionBridge`, `NestBridge`, `MarkChatRoomsAsRead`, `RowUnread`, + `ReloadMintViewModel`, `EventSync`, `CardFeedContentState`, `RoleBasedHttpClientBuilder`. + - **Payment and intent handlers:** `ZapPaymentHandler`, `V4VPaymentHandler`, + `LightningAddressResolver`, `MeltProcessor`, `ZapCustomDialog.payViaIntent`. They take an + Android `Context` to fire payment intents; that needs a `PaymentLauncher` port. + - **Composable files it borrows a type or constant from:** `ZapAmountCommentNotification` + (`MultiSetCompose`), `ZapraiserStatus` (`ReactionsRow`), `NOTIFICATION_LAST_READ_KEY` + (`NotificationScreen`). The declarations move to `commons`; the composables don't have to. + - **Genuinely Android:** `MediaSaverToDisk`, `MarmotGroupIconUploader`, + `dismissNotificationForEvent` (`NotificationUtils`). These go behind ports. + +`AccountViewModel` moves after the `Account` group, into `commons/viewmodels` (jvmAndroid +first). + +## Sequence + +1. **Docs** (this plan, and the rule changes in CLAUDE.md, both ARCHITECTURE files, three + skills and the tracker). Done 2026-09-27. +2. **Cut the `Account` group's seams**, one small PR each, in the app, with no move yet. Every + cut is a behaviour-preserving refactor that compiles and tests on its own: + - the `BuildConfig` parameter; + - `DefaultBottomBarEntries`; + - `MembershipNotificationKinds`; + - `logTime`; + - `checkNotInMainThread`; + - the `LocationResult` and `FileHeader` types; + - the settings-persister, Geohash legacy-store, Nip46-bridge and notification-dispatcher + ports; + - the typed zap error. +3. **Move the group** (77 files) to `commons/jvmAndroid` in one PR. Desktop keeps its + `DesktopIAccount` until the old app is retired; `IAccount` stays as the port it already is. +4. **`AccountViewModel`**: the same recipe, using the dependency list above. +5. **The shared composables and their helpers** (sized in the tracker's 2026-09-27 section): + - `RouteMaker`; + - drop the `accountViewModel` overloads of the `observe*` helpers; + - `DisappearingScaffold`'s immersive-scrolling read goes onto `DisplaySettings`; + - then `UserProfilePicture`, `UsernameDisplay`, `Loaders`, `RichTextViewer`, + `NoteCompose`. + + Once `AccountViewModel` is in `commons`, these move without retyping. +6. **Screens**, feature by feature, into `commonsUI`. +7. **Navigation**: the library swap, then `AppNavigation` + rail + drawer + bottom bar. +8. **The app root port** and the new JVM shim. Then the Desktop feature inventory, and + retiring the old `desktopApp`. + +Steps 2–5 can interleave. Step 5's helpers can start before 3–4 if they take `Account` / +`AccountViewModel` unchanged and only move later. + +## Method (so the numbers can be re-run) + +The closure script indexes every public top-level declaration in +`amethyst/src/{main,play,fdroid}`. For each file it follows edges of four kinds: + +- explicit imports; +- wildcard imports; +- inline fully-qualified names; +- **same-package references**, which need no import. This is the undercount the tracker's + header warns about. + +A same-package lowercase name reached through a `.` counts only when it is an extension. The +lexer blanks strings but keeps `${…}` templates. A file is marked blocked by any of: + +- `android.*`, `com.google.*`, or a non-KMP `androidx.*`; +- `R`/`BuildConfig`; +- a library `commons` lacks. + +Symbols from `commons.*` that actually live in `commonsUI` (`Res`, `loadStringRes`) are +checked separately. The first pass missed them. Known over-count: same-package token matching +can link a file to a same-named declaration it does not use; the edges above were checked by +hand. diff --git a/commons/plans/README.md b/commons/plans/README.md index faf2fdd620..46009a7720 100644 --- a/commons/plans/README.md +++ b/commons/plans/README.md @@ -1,13 +1,14 @@ # commons plans -_Audited 2026-06-30 (+ 2026-09-12 split entry, 2026-09-27 migration entries)._ +_Audited 2026-06-30 (+ 2026-09-12 split entry, 2026-09-27 migration and one-UI entries)._ ## In progress | Plan | Summary | | ---- | ------- | | [2026-05-04-custom-feeds-plan.md](2026-05-04-custom-feeds-plan.md) | Custom feed creation/discovery/management for Desktop; core model + builder + kind 31890 + desktop UI shipped, but relay-filter layer, DVM marketplace, kind 10090 sync, and list resolution still pending. | | [2026-05-06-nest-subscription-manager-extraction.md](2026-05-06-nest-subscription-manager-extraction.md) | Split the per-speaker subscription state machine out of `NestViewModel`; only the `ActiveSubscription` stepping-stone is extracted so far. | -| [2026-08-30-commons-migration-sweep.md](2026-08-30-commons-migration-sweep.md) | The running tracker for moving `amethyst/` code into `commons`/`commonsUI`: waves, what moved each round, what stays and why. `LocalCache` has moved; retiring `DesktopLocalCache` (Wave 2 part B) and the `AccountViewModel` hub composables are next. | +| [2026-09-27-one-ui-android-desktop.md](2026-09-27-one-ui-android-desktop.md) | **The target.** Android ships on laptops, so the whole UI (screens + navigation shell) moves to `commonsUI`, `amethyst` becomes an Android shim, and a new JVM `desktopApp` renders the same UI. Measures Wave 4: `Account`'s 77-file move-group, its 5 blocked files and 14 seams, and `AccountViewModel`'s dependencies; sequences the rest. | +| [2026-08-30-commons-migration-sweep.md](2026-08-30-commons-migration-sweep.md) | The running log of moving `amethyst/` code into `commons`/`commonsUI`: waves, what moved each round. `LocalCache` has moved. Its STAY list and Wave 2 part B are superseded by the one-UI plan above. | | [2026-05-30-amethyst-to-commons-migration.md](2026-05-30-amethyst-to-commons-migration.md) | The original roadmap for the same move; superseded in practice by the sweep tracker above. | ## Queued diff --git a/commonsUI/ARCHITECTURE.md b/commonsUI/ARCHITECTURE.md index 8a091c05f7..a1e4e707b7 100644 --- a/commonsUI/ARCHITECTURE.md +++ b/commonsUI/ARCHITECTURE.md @@ -17,6 +17,12 @@ therefore useless to the headless `cli`: its `LazyListState`, `ChatNewMessageState` with `TextFieldValue`, `EmojiSuggestionState` with `TextFieldState`). +Its end state is **the whole app UI**: every screen, the navigation host and the +navigation chrome for every window size (bottom bar, rail, permanent drawer), with +`amethyst` and a new JVM `desktopApp` as thin shims around it. Screens still in +`amethyst/` are waiting on `AccountViewModel` and the app root, not staying there +by design. See `commons/plans/2026-09-27-one-ui-android-desktop.md`. + It depends on `:commons` (and `:quartz`) as **`api`**, so a consumer that adds `:commonsUI` sees the headless layer transitively. `amethyst`, `desktopApp`, `nappletHost` and `benchmark` depend on it; `cli`, `geode`, `marmotBench`