From 8209f4416a253ea40eb9fab4b647b0dcb092ed88 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 22:01:29 +0000 Subject: [PATCH] docs: fix stale claims in Claude skills against current code Audit pass that verified every concrete claim in .claude/ against the repository: - account-state: Account.kt no longer exposes followListFlow-style StateFlows; document the state-object pattern (kind3FollowList, muteList, bookmarkState, ... each exposing .flow) and rewrite the catalog reference from the real Account.kt - feed-patterns: filter bases (FeedFilter, AdditiveFeedFilter, ChangesFlowFilter, FeedContentState) moved to commons/ui/feeds; ui/dal keeps AdditiveComplexFeedFilter/FilterByListParams plus back-compat typealiases; fix recipe example signatures - relay-client: add nip17Dm/, eoseManagers and subscriptions entries to the layout tree - gradle-expert: 4-module claim -> 10 modules; refresh compose/kotlin/ BOM versions; rewrite dependency graph with verified edges for cli, geode, quic, nestsClient, quic-interop, benchmark - desktop-expert: drop drifted Main.kt line numbers; sidebar is the custom MainSidebar in DeckSidebar.kt, not a NavigationRail in SinglePaneLayout.kt - android-expert: compileSdk/targetSdk 36 -> 37, versionName via generateVersionName() - kotlin-expert: remove reference to nonexistent commit 258c4e011 - CLAUDE.md: add missing geode/benchmark/quic-interop modules - desktop-run: packageRpm + correct binaries output path; extract.md: drop duplicated find clause - session-start.sh: /home/user/Amber fallback was a copy-paste from another repo; fall back to CLAUDE_PROJECT_DIR https://claude.ai/code/session_01EC7LdXjatFTh1CJSP4qKRn --- .claude/CLAUDE.md | 7 +- .claude/commands/desktop-run.md | 6 +- .claude/commands/extract.md | 2 +- .claude/hooks/session-start.sh | 2 +- .claude/skills/account-state/SKILL.md | 48 +++--- .../references/account-state-flow.md | 149 +++++++++--------- .claude/skills/android-expert/SKILL.md | 6 +- .claude/skills/desktop-expert/SKILL.md | 8 +- .../references/desktop-navigation.md | 4 +- .claude/skills/feed-patterns/SKILL.md | 58 ++++--- .../references/feed-filter-composition.md | 13 +- .claude/skills/gradle-expert/SKILL.md | 12 +- .../references/dependency-graph.md | 118 +++++++++----- .claude/skills/kotlin-expert/SKILL.md | 5 +- .claude/skills/relay-client/SKILL.md | 10 +- 15 files changed, 252 insertions(+), 196 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 46f21f1699..c1ba392b9d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -12,8 +12,10 @@ architecture while sharing the back end components with the android counterpart. 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 -exists. `nestsClient` runs the audio-room protocol on top of `:quic` for the NIP-53 -audio-rooms feature. It implements both IETF `draft-ietf-moq-transport-17` (under +exists. `geode` is a standalone JVM Nostr relay (Ktor) built on quartz's +relay-server code; smaller modules are `benchmark` (Android macrobenchmarks) and +`quic-interop` (QUIC interop runner, lives at `quic/interop`). `nestsClient` runs +the audio-room protocol on top of `:quic` for the NIP-53 audio-rooms feature. It implements both IETF `draft-ietf-moq-transport-17` (under `moq/`) and **moq-lite Lite-03** (kixelated's variant, under `moq/lite/`); the production listener AND speaker paths both run on moq-lite to interop with the nostrnests reference relay. The IETF code is kept as a reference + unit-test @@ -55,6 +57,7 @@ amethyst/ │ └── src/ │ ├── commonMain/ # MoQ session, NestsListener, audio glue │ └── jvmAndroid/ # Opus encode/decode, AudioRecord/AudioTrack +├── geode/ # Standalone JVM Nostr relay (Ktor) on quartz's relay-server code ├── desktopApp/ # Desktop JVM application (layouts, navigation) ├── amethyst/ # Android app (layouts, navigation) └── cli/ # Amy — non-interactive CLI (JVM only, no Compose) diff --git a/.claude/commands/desktop-run.md b/.claude/commands/desktop-run.md index 6b43f0d2bf..0b1f5eacb1 100644 --- a/.claude/commands/desktop-run.md +++ b/.claude/commands/desktop-run.md @@ -12,7 +12,7 @@ Build and run the Amethyst Desktop application: If the build fails, check: -1. **JDK Version**: Requires JDK 17+ +1. **JDK Version**: Requires JDK 21+ (`jvmToolchain(21)` in `desktopApp/build.gradle.kts`) ```bash java -version ``` @@ -39,7 +39,7 @@ If the build fails, check: ./gradlew :desktopApp:packageMsi # Linux -./gradlew :desktopApp:packageDeb +./gradlew :desktopApp:packageDeb # or :desktopApp:packageRpm ``` -Outputs will be in `desktopApp/build/compose/binaries/` +Outputs will be in `desktopApp/build/compose/binaries/main/` diff --git a/.claude/commands/extract.md b/.claude/commands/extract.md index 6e9eaf1bca..c9648a01ab 100644 --- a/.claude/commands/extract.md +++ b/.claude/commands/extract.md @@ -8,7 +8,7 @@ Extract the component `$ARGUMENTS` from the Android app to shared KMP code: 1. **Locate the component** in the amethyst module: ```bash - find amethyst/src -name "*$ARGUMENTS*" -o -name "*$ARGUMENTS*" + find amethyst/src -name "*$ARGUMENTS*" grep -r "fun $ARGUMENTS\|class $ARGUMENTS" amethyst/src/ ``` diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 5b1409249a..d2e869fb85 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -160,7 +160,7 @@ echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licens echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" # Create local.properties if missing -REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-$PWD}")" LOCAL_PROPS="$REPO_ROOT/local.properties" if [ ! -f "$LOCAL_PROPS" ]; then echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" diff --git a/.claude/skills/account-state/SKILL.md b/.claude/skills/account-state/SKILL.md index ec370630eb..528a2dd273 100644 --- a/.claude/skills/account-state/SKILL.md +++ b/.claude/skills/account-state/SKILL.md @@ -1,6 +1,6 @@ --- name: account-state -description: Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user StateFlow properties — follow list, relays, settings, mutes, bookmarks), `LocalCache` (the object-level event store backed by `LargeCache`), `User`/`Note` model classes, or any ViewModel that reads user-specific state. Covers how account events cascade from relay arrival to UI state, how to add a new account-scoped setting, and when to read from `LocalCache` vs subscribe to a StateFlow. +description: Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by `LargeCache`), `User`/`Note` model classes, or any ViewModel that reads user-specific state. Covers how account events cascade from relay arrival to UI state, how to add a new account-scoped setting, and when to read from `LocalCache` vs subscribe to a StateFlow. --- # Account & Local Cache State @@ -21,10 +21,10 @@ The backbone of Amethyst's client state: one `Account` per signed-in user, plus Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow emits change │ ▼ - Account observes relevant kinds (3, 10002, 10000, …) + Account state objects pin the relevant addressable notes │ ▼ - Account StateFlow updates (followList, relays, mutes, …) + State-object `.flow` updates (kind3FollowList, nip65RelayList, muteList, …) │ ▼ ViewModels collect @@ -33,22 +33,22 @@ Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow e Composables render ``` -`LocalCache` is the event store. `Account` is the *derived* per-user view (follow list, relays, mutes, emojis, bookmarks, etc.). UI listens to `Account`'s StateFlows, not directly to `LocalCache`, except for note-level rendering. +`LocalCache` is the event store. `Account` is the *derived* per-user view (follow list, relays, mutes, emojis, bookmarks, etc.). UI listens to the `.flow` of `Account`'s state objects, not directly to `LocalCache`, except for note-level rendering. ## Key Files ### `Account.kt` (singleton-per-session) -- `class Account(...)` — holds 50+ StateFlow properties, each wired to a specific Nostr kind: - - `followListFlow` ← NIP-02 ContactList (kind 3) - - `relayListFlow` ← NIP-65 RelayList (kind 10002) - - `muteListFlow` ← NIP-51 Lists (kind 10000) - - `bookmarkListFlow` ← NIP-51 Lists (kind 10003) - - `topNavFeedsFlow`, `marmotGroupsFlow`, `customEmojisFlow`, `privateBookmarksFlow`, etc. - - Settings: `defaultZapAmountsFlow`, `theme`, `language`, `proxyFlow`, `showSensitiveContentFlow`, … -- Each flow has a private `MutableStateFlow` and a public read-only `StateFlow` view. Mutation goes through specific methods (`sendPost`, `follow(pubKey)`, `addBookmark(...)`) that both update the flow and publish the signed replaceable event. -- Uses `CoroutinesExt.launchIO` for network / crypto; UI reads via `collectAsStateWithLifecycle` on Android and `collectAsState` on Desktop. -- Sibling files per feature live alongside: `AccountSettings.kt`, `AccountSyncedSettings.kt`, plus per-NIP state objects under `model/nip02FollowLists/`, `model/nip51Lists/`, `model/nip65RelayList/`, etc. +- `class Account(...)` — holds 50+ **state objects**, one per feature, each wired to a specific Nostr kind: + - `kind3FollowList = Kind3FollowListState(...)` ← NIP-02 ContactList (kind 3) + - `nip65RelayList = Nip65RelayListState(...)` ← NIP-65 RelayList (kind 10002), plus siblings `dmRelayList`, `searchRelayList`, `blockedRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, … + - `muteList = MuteListState(...)` ← NIP-51 MuteList (kind 10000) + - `bookmarkState = BookmarkListState(...)` ← NIP-51 Bookmarks (kind 10003), plus `labeledBookmarkLists`, `pinState`, `interestSets`, `peopleLists`, `followLists`, `hashtagList`, `geohashList`, `communityList`, `emoji`, `blossomServers`, … + - Derived/merged views: `hiddenUsers`, `allFollows`, `homeRelays`, `outboxRelays`, `dmRelays`, `notificationRelays`, `trustedRelays`, and the `live*FollowListsPerRelay` outbox loaders. +- **The pattern:** each `XState` class pins its addressable note via `cache.getOrCreateAddressableNote(address)` (a long-term reference so GC/eviction can't drop it), exposes `val flow: StateFlow<…>` derived from the note's metadata flow (decrypted through a per-feature `DecryptionCache`, with backup fallback from `AccountSettings`, `stateIn(scope, Eagerly, …)`), and offers suspend mutation helpers (e.g. `MuteListState.hideUser(pubkey)`) that build the updated signed event. Consumers read `account.muteList.flow`, never a raw `MutableStateFlow` on `Account`. +- Encrypted lists pair the state object with a `DecryptionCache` sibling (`muteListDecryptionCache`, `peopleListDecryptionCache`, …) so NIP-44 decryption results are cached per event. +- UI reads via `collectAsStateWithLifecycle` on Android and `collectAsState` on Desktop. +- Sibling files per feature live alongside: `AccountSettings.kt`, `AccountSyncedSettings.kt`, plus per-NIP state classes under `model/nip02FollowLists/`, `model/nip51Lists/`, `model/nip65RelayList/`, etc. ### `LocalCache.kt` @@ -72,31 +72,29 @@ Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow e Typical recipe: 1. If the setting is persisted as a Nostr event, pick the right kind (e.g. NIP-51 list, NIP-78 app-specific data, NIP-65 relay list). -2. Add a model folder under `amethyst/.../model/nipXX…/` with an `ExtState`/builder class if needed. -3. In `Account.kt`: - - Add a private `MutableStateFlow`. - - Expose a `StateFlow` read view. - - Subscribe to the relay (via the relayClient subscription pattern — see `relay-client` skill). - - On event arrival, parse with the quartz event class and update the flow. - - Write a mutation method (`updateX(...)`) that builds a new event via the corresponding `TagArrayBuilder`, signs through `NostrSigner`, and publishes. -4. Add UI that `collect`s the flow. Settings screens live in `amethyst/.../ui/screen/loggedIn/settings/`. +2. Add a model folder under `amethyst/.../model/nipXX…/` with an `XState` class modeled on an existing one (`MuteListState` for an encrypted list, `BookmarkListState` for a plain one): + - Pin the addressable note: `val xNote = cache.getOrCreateAddressableNote(XEvent.createAddress(signer.pubKey))`. + - Expose `val flow: StateFlow<…>` mapped from `xNote.flow().metadata.stateFlow`, decrypting through a per-feature `DecryptionCache` if the list is private, with backup fallback from `AccountSettings`, then `stateIn(scope, Eagerly, default)`. + - Add suspend mutation helpers that build the updated event via the quartz event class (`XEvent.add/remove/create`) and return it signed. +3. In `Account.kt`, instantiate the state object (and its `DecryptionCache` sibling if encrypted) as a `val`. Publishing the returned event goes through `Account`'s send path; the relay subscription side is the relayClient pattern (see `relay-client` skill). +4. Add UI that `collect`s `account.x.flow`. Settings screens live in `amethyst/.../ui/screen/loggedIn/settings/`. ## `LocalCache` vs `Account` Flow — Which to Read? - **Are you rendering a specific note / user you hold an id for?** → `LocalCache.getOrCreateNote(id)` + collect `note.flowSet.metadata`. -- **Are you rendering "my follows", "my mutes", "my relays"?** → `Account.`. +- **Are you rendering "my follows", "my mutes", "my relays"?** → `account..flow` (e.g. `account.kind3FollowList.flow`, `account.muteList.flow`, `account.nip65RelayList.flow`). - **Are you rendering a feed?** → Use a `FeedFilter` + `FeedViewModel` (see `feed-patterns` skill). Don't scan `LocalCache` in a composable. ## Gotchas - **`LocalCache` is a singleton across accounts.** Switching accounts doesn't wipe it — `Account` re-derives its flows from the same cache. - **Don't store Flows inside `Note` / `User`** expecting them to survive eviction. Eviction drops the whole object. -- **Mutations to `Account` flows must also publish the signing event.** A flow update without a publish means other clients won't see it. +- **State-object mutation helpers return a signed event — publishing it is the caller's job.** A locally updated list without a publish means other clients won't see it. - **`Note` is mutable** — treat instances as identity-based (same id → same Note). Use `.flowSet` when you need reactive state. - **`MemoryTrimmingService` can evict aggressively** on Android under pressure. Don't assume a previously-seen note is still resident. ## References -- `references/account-state-flow.md` — catalog of major `Account` StateFlow properties and their source kinds. +- `references/account-state-flow.md` — catalog of major `Account` state objects and their source kinds. - `references/local-cache.md` — `LocalCache` internals, insertion path, indexes. - Complements: `nostr-expert` (event parsing), `relay-client` (subscription wiring), `feed-patterns` (how feeds consume this state), `auth-signers` (how mutation signs events). diff --git a/.claude/skills/account-state/references/account-state-flow.md b/.claude/skills/account-state/references/account-state-flow.md index e9bf1256f3..d7e37c5524 100644 --- a/.claude/skills/account-state/references/account-state-flow.md +++ b/.claude/skills/account-state/references/account-state-flow.md @@ -1,93 +1,94 @@ -# Account StateFlow Catalog +# Account State-Object Catalog -`Account.kt` exposes dozens of `StateFlow` properties that mirror different facets of the current user. This is a map from flow → Nostr kind → model package. +`Account.kt` composes ~50 **feature state objects** (not raw StateFlow +properties). Each object pins its backing addressable note in `LocalCache`, +exposes `val flow: StateFlow<…>` (decrypted + backup-merged + `stateIn`), and +offers suspend mutation helpers that return signed events. Consumers read +`account..flow`. -(Flow names are exact as of the current `Account.kt`; if a flow has been renamed, grep `Account.kt` for the old name.) +(Property and class names are exact as of the current `Account.kt`; if one has +been renamed, grep `Account.kt` for the class name.) ## Identity & Contacts -| Flow | Kind(s) | Source | Model package | -|------|---------|--------|---------------| -| `userProfile().liveMetadata` | 0 MetadataEvent | relay | `model/nip01UserMetadata/` | -| `followListFlow` | 3 ContactListEvent | relay | `model/nip02FollowLists/` | -| `followersFlow` | derived | LocalCache scan | — | -| `muteListFlow` | 10000 NIP-51 | relay | `model/nip51Lists/` | -| `blockListFlow` | 10000 list variant | relay | `model/nip51Lists/` | +| Account property | State class | Kind | Package | +|------------------|-------------|------|---------| +| `userMetadata` | `UserMetadataState` | 0 | `amethyst/.../model/nip01UserMetadata/` | +| `kind3FollowList` | `Kind3FollowListState` | 3 | `model/nip02FollowLists/` | +| `muteList` (+ `muteListDecryptionCache`) | `MuteListState` | 10000 | `model/nip51Lists/muteList/` | +| `blockPeopleList`, `peopleLists` | `BlockPeopleListState`, `PeopleListsState` | NIP-51 people sets | `model/nip51Lists/peopleList/` | +| `followLists` | `FollowListsState` | NIP-51 follow sets | `model/nip51Lists/peopleList/` | +| `hiddenUsers` | `HiddenUsersState` — derived from `muteList.flow` + `blockPeopleList.flow` | — | `model/nip51Lists/` | +| `allFollows` | `MergedFollowListsState` — merges kind3 + people/follow/hashtag/geohash/community lists | — | `model/serverList/` | -## Relays & Connectivity +## Relay Lists -| Flow | Kind | Package | -|------|------|---------| -| `relayListFlow` | 10002 RelayList (NIP-65) | `model/nip65RelayList/` | -| `dmRelayListFlow` | 10050 | `model/nip65RelayList/` | -| `searchRelayListFlow` | 10007 | `model/nip65RelayList/` | -| `nip86RelayListFlow` | NIP-86 relay management | `model/nip86RelayManagement/` | -| `proxyFlow`, `torStateFlow` | local preferences | `model/torState/`, `AccountSyncedSettings` | +| Account property | State class | Kind | Package | +|------------------|-------------|------|---------| +| `nip65RelayList` | `Nip65RelayListState` | 10002 | `model/nip65RelayList/` | +| `dmRelayList` | `DmRelayListState` | 10050 | `model/nip17Dms/` | +| `searchRelayList` | `SearchRelayListState` | 10007 | `model/nip51Lists/searchRelays/` | +| `blockedRelayList` | `BlockedRelayListState` | 10006 | `model/nip51Lists/blockedRelays/` | +| `localRelayList` | `LocalRelayListState` | local | `model/localRelays/` | +| `privateStorageRelayList` | `PrivateStorageRelayListState` | private storage | `model/edits/` | +| `keyPackageRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, `relayFeedsList` | per-feature `…RelayListState` classes, each with a `DecryptionCache` sibling | custom relay sets | `model/nip51Lists/…` | + +Derived relay views (merge several of the above): `homeRelays` +(`AccountHomeRelayState`), `outboxRelays`, `dmRelays`, `notificationRelays`, +`trustedRelays`, `followPlusAllMineWithIndex`, `followPlusAllMineWithSearch`, +`defaultGlobalRelays`. ## Content Lists -| Flow | Kind | Package | -|------|------|---------| -| `bookmarkListFlow` | 10003 | `model/nip51Lists/` | -| `privateBookmarksFlow` | encrypted list | `model/nip51Lists/` | -| `topNavFeedsFlow` | custom | `model/topNavFeeds/` | -| `customEmojisFlow` | 10030 NIP-30 | `model/nip30CustomEmojis/` | -| `marmotGroupsFlow` | NIP-29 (marmot variant) | `model/marmot/` | -| `nip72CommunitiesFlow` | 34550 (NIP-72) | `model/nip72Communities/` | -| `nip64ChessFlow` | NIP-64 chess games | `model/nip64Chess/` | +| Account property | State class | Kind | Package | +|------------------|-------------|------|---------| +| `bookmarkState` (and legacy `oldBookmarkState`) | `BookmarkListState` | 10003 | `model/nip51Lists/` | +| `labeledBookmarkLists` | `LabeledBookmarkListsState` | NIP-51 bookmark sets | `model/nip51Lists/labeledBookmarkLists/` | +| `pinState` | `PinListState` | NIP-51 | `model/nip51Lists/` | +| `interestSets` | `InterestSetsState` | NIP-51 interest sets | `model/nip51Lists/interestSets/` | +| `hashtagList` / `geohashList` | `HashtagListState` / `GeohashListState` | NIP-51 | `model/nip51Lists/hashtagLists/`, `…/geohashLists/` | +| `communityList` | `CommunityListState` | NIP-72 communities | `model/nip72Communities/` | +| `favoriteAlgoFeedsList` | `FavoriteAlgoFeedsListState` | NIP-51 | `model/nip51Lists/` | +| `emoji`, `ownedEmojiPacks` | `EmojiPackState`, `OwnedEmojiPacksState` | 10030 | `commons/.../commons/model/nip30CustomEmojis/` | +| `publicChatList` | `PublicChatListState` | NIP-28 | `commons/.../commons/model/nip28PublicChats/` | +| `ephemeralChatList` | `EphemeralChatListState` | ephemeral chats | `commons/.../commons/model/emphChat/` | +| `blossomServers` | `BlossomServerListState` | Blossom (BUD) | `model/nipB7Blossom/` | -## Messaging +## Other Feature State -| Flow | Kind | Package | -|------|------|---------| -| `dmInboxFlow` | 14 / 1059 (NIP-17 / gift-wrap) | `model/nip17Dms/` | -| `nwcSettingsFlow` | NIP-47 wallet connect | `model/nip47WalletConnect/` | -| `paymentTargetsFlow` | NIP-A3 | `model/nipA3PaymentTargets/` | -| `blossomServersFlow` | NIP-B7 blossom | `model/nipB7Blossom/` | +| Account property | State class | Purpose | Package | +|------------------|-------------|---------|---------| +| `vanish` | `VanishRequestsState` | NIP-62 vanish requests | `model/nip62Vanish/` | +| `appSpecific` | `AppSpecificState` | NIP-78 app data | `model/nip78AppSpecific/` | +| `otsState` | `OtsState` | NIP-03 OpenTimestamps | `model/nip03Timestamp/` | +| `live*FollowListsPerRelay` | `OutboxLoaderState(...).flow` — already a flow | per-feed outbox routing | `model/topNavFeeds/` | +| `privateDMDecryptionCache`, `draftsDecryptionCache` | `PrivateDMCache`, `DraftEventCache` | NIP-44 decryption caches | — | -## Settings & UI - -| Flow | Source | Package | -|------|--------|---------| -| `uiSettingsFlow` | local | `model/UiSettings.kt`, `UiSettingsFlow.kt` | -| `antiSpamFilter` | local | `model/AntiSpamFilter.kt` | -| `privacyOptionsFlow` | local | `model/privacyOptions/` | -| `trustedAssertionsFlow` | derived | `model/trustedAssertions/` | -| `defaultZapAmountsFlow`, `theme`, `language` | local preferences | `AccountSettings.kt`, `AccountSyncedSettings.kt` | - -## Advanced / Derived - -| Flow | Purpose | Package | -|------|---------|---------| -| `accountsCacheFlow` | multi-account switcher | `model/accountsCache/` | -| `algoFeedsFlow` | custom algorithmic feeds | `model/algoFeeds/` | -| `vanishFlow` | NIP-62 account vanish requests | `model/nip62Vanish/` | -| `nip78AppSpecificFlow` | NIP-78 app-specific data | `model/nip78AppSpecific/` | -| `serverListFlow` | media/upload servers | `model/serverList/` | +Note the migration direction: newer/extracted state classes live in +`commons/src/commonMain/.../commons/model/`, the rest still in +`amethyst/src/main/java/.../model/`. Check both when looking for one. ## Publishing Mutations -Every flow has a corresponding mutation method on `Account` that: +State objects' mutation helpers (e.g. `MuteListState.hideUser(pubkey)`, +`BookmarkListState` add/remove) **build and sign** the updated replaceable +event via the quartz event class (`XEvent.add / remove / create`) and return +it. The caller (usually a method on `Account`) is responsible for sending it +through the client. Decryption results are cached in the paired +`…DecryptionCache` so re-renders don't re-decrypt. -1. Constructs the updated event using a `TagArrayBuilder`. -2. Signs through the injected `NostrSigner` (see `auth-signers` skill). -3. Publishes to the appropriate relay set. -4. Updates the local StateFlow *before* relay round-trip (optimistic). -5. Rolls back / reconciles on failure. +## When a State Object Doesn't Exist Yet -Examples of mutation methods (names may vary slightly in current code): -- `follow(pubKey)` / `unfollow(pubKey)` -- `addBookmark(noteId)` / `removeBookmark(noteId)` -- `mute(pubKey)` / `unmute(pubKey)` -- `updateRelayList(...)`, `updateDmRelayList(...)` -- `sendPost(...)`, `sendReaction(...)`, `sendZap(...)` +If you're adding a new NIP that's user-scoped, follow the pattern (full recipe +in `SKILL.md`): -## When a Flow Doesn't Exist Yet - -If you're adding a new NIP that's user-scoped, follow the pattern: - -1. Create `model/nipXX…/` with an optional `ExtState`/builder class. -2. Add `private val _xFlow = MutableStateFlow(initial)` + `val xFlow: StateFlow = _xFlow.asStateFlow()` to `Account`. -3. Wire the relay subscription (see `relay-client` skill). -4. Add the mutation method that builds, signs, and publishes. -5. Update persistence if the setting is local-only (`AccountSettings.kt`). +1. Create `model/nipXX…/XState.kt` modeled on `MuteListState` (encrypted) or + `BookmarkListState` (plain). +2. Pin the note with `cache.getOrCreateAddressableNote(...)`, expose + `val flow: StateFlow<…>` via `stateIn(scope, Eagerly, default)`. +3. Instantiate it in `Account.kt` (plus a `DecryptionCache` sibling if + private), and wire the relay subscription (see `relay-client` skill). +4. Add mutation helpers that build, sign, and return the event; publish from + the calling site. +5. Use `AccountSettings` for the local backup copy if the list must survive + relay loss. diff --git a/.claude/skills/android-expert/SKILL.md b/.claude/skills/android-expert/SKILL.md index db0a32c4b0..02b33e8158 100644 --- a/.claude/skills/android-expert/SKILL.md +++ b/.claude/skills/android-expert/SKILL.md @@ -744,14 +744,14 @@ fun SignerIntegration(accountViewModel: AccountViewModel) { ```gradle android { namespace = 'com.vitorpamplona.amethyst' - compileSdk = 36 + compileSdk = 37 // from libs.versions.toml android-compileSdk — check there, it drifts defaultConfig { applicationId = "com.vitorpamplona.amethyst" minSdk = 26 // Android 8.0 (Oreo) - targetSdk = 36 // Android 15 + targetSdk = 37 // android-targetSdk in libs.versions.toml versionCode = 447 - versionName = "1.11.0" + versionName = generateVersionName(libs.versions.app.get(), rootDir) vectorDrawables { useSupportLibrary = true diff --git a/.claude/skills/desktop-expert/SKILL.md b/.claude/skills/desktop-expert/SKILL.md index b4208def3f..477880202d 100644 --- a/.claude/skills/desktop-expert/SKILL.md +++ b/.claude/skills/desktop-expert/SKILL.md @@ -74,7 +74,7 @@ fun main() = application { - `rememberWindowState()` manages size/position - `onCloseRequest` handles window close -**See:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt` — `fun main()` at L172, `application {` at L186, top-level `Window` at L229, `MenuBar` at L234. +**See:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt` — grep for `fun main()`, `application {`, the top-level `Window`, and `MenuBar {` (the file is large and line numbers drift; navigate by symbol). --- @@ -280,9 +280,9 @@ Row(Modifier.fillMaxSize()) { } ``` -**See:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt` (NavigationRail at L97, items at L103+). `DeckLayout` alongside it handles multi-pane workspaces. +**In Amethyst Desktop:** the sidebar is the custom `MainSidebar` composable in `desktopApp/.../ui/deck/DeckSidebar.kt`, instantiated from `Main.kt` and shared by both layout modes (`SinglePaneLayout` and the multi-pane `DeckLayout` alongside it). It is hand-rolled, not Material's `NavigationRail` — use `NavigationRail` only for new, simpler cases. -**Why NavigationRail?** +**Why a left sidebar?** - Desktop has horizontal space (1200+ dp width) - Vertical sidebar is standard desktop pattern - Always visible (no tabs hidden) @@ -290,7 +290,7 @@ Row(Modifier.fillMaxSize()) { **Android comparison:** - Android: `BottomNavigationBar` (horizontal, bottom) -- Desktop: `NavigationRail` (vertical, left) +- Desktop: left vertical sidebar (`MainSidebar`) ### Multi-Pane Layouts diff --git a/.claude/skills/desktop-expert/references/desktop-navigation.md b/.claude/skills/desktop-expert/references/desktop-navigation.md index 38011f82cb..ad829582f0 100644 --- a/.claude/skills/desktop-expert/references/desktop-navigation.md +++ b/.claude/skills/desktop-expert/references/desktop-navigation.md @@ -11,11 +11,11 @@ Comparison of mobile vs desktop navigation patterns in AmethystMultiplatform. --- -## Desktop: NavigationRail +## Desktop: Left Sidebar ### Current Implementation -**File:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt` (NavigationRail begins at L97; `NavigationRailItem`s at L103 and L127+). +**File:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckSidebar.kt` — the custom `MainSidebar` composable, instantiated from `Main.kt` and shared by both `SinglePaneLayout` and the multi-pane `DeckLayout`. Amethyst Desktop does **not** use Material's `NavigationRail`; the snippet below shows the generic Compose pattern for reference, useful for simpler new surfaces. ```kotlin @Composable diff --git a/.claude/skills/feed-patterns/SKILL.md b/.claude/skills/feed-patterns/SKILL.md index 7526d5d34e..aada17ce3c 100644 --- a/.claude/skills/feed-patterns/SKILL.md +++ b/.claude/skills/feed-patterns/SKILL.md @@ -1,6 +1,6 @@ --- name: feed-patterns -description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with `FeedFilter` / `AdditiveComplexFeedFilter` / `ChangesFlowFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose. +description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with the shared `FeedFilter` / `AdditiveFeedFilter` / `ChangesFlowFilter` / `FeedContentState` in `commons/.../ui/feeds/`, the Android-only `AdditiveComplexFeedFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose. --- # Feed Patterns @@ -24,27 +24,33 @@ Amethyst's "feed" abstraction is: a `FeedFilter` that decides which notes belong │ ◄── ChatroomFeedViewModel │ │ ◄── MarmotGroupFeedViewModel │ │ │ -│ FeedContentState — the flow the UI collects │ +│ │ +│ commons/.../ui/feeds/ (shared, KMP) │ +│ IFeedFilter / FeedFilter (abstract base) │ +│ IAdditiveFeedFilter / AdditiveFeedFilter │ +│ ChangesFlowFilter │ +│ FeedContentState, FeedState — the flow the UI collects │ └─────────────────────────────────────────────────────────────┘ ▲ │ uses │ ┌─────────────────────────────────────────────────────────────┐ -│ amethyst/.../ui/dal/ (Android; feeds defined per screen) │ -│ FeedFilter (abstract) │ +│ amethyst/.../ui/dal/ (Android-only additions) │ │ AdditiveComplexFeedFilter │ -│ ChangesFlowFilter │ │ FilterByListParams │ -│ DefaultFeedOrder │ +│ DefaultFeedOrder (Note/Event/Card comparators) │ +│ (FeedFilters.kt & ChangesFlowFilter.kt here are just │ +│ back-compat typealiases re-exporting commons) │ │ │ -│ Plus concrete feeds: HomeFeedFilter, HashtagFeedFilter, │ -│ BookmarkListFeedFilter, NotificationFeedFilter, … │ +│ Concrete feeds: HomeNewThreadFeedFilter, │ +│ HashtagFeedFilter, NotificationFeedFilter, … live in │ +│ feature folders under ui/screen/loggedIn/*/dal/ │ └─────────────────────────────────────────────────────────────┘ ▲ │ reads │ ┌─────────────────────────────────────────────────────────────┐ -│ model/LocalCache.kt + Account. │ +│ model/LocalCache.kt + account..flow │ └─────────────────────────────────────────────────────────────┘ ``` @@ -60,25 +66,33 @@ Amethyst's "feed" abstraction is: a `FeedFilter` that decides which notes belong - **`MarmotGroupFeedViewModel.kt`** — NIP-29 / marmot group feed. - **`LiveStreamTopZappersViewModel.kt`, `SearchBarState.kt`, `ChatNewMessageState.kt`** — narrower, non-feed states that share the plumbing. -### Android DAL (the filters) +### Shared filter bases (commons) + +`commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/`: + +- **`FeedFilter.kt`** — `abstract class FeedFilter : IFeedFilter`. Has `feed(): List` (the sync query against the cache), `feedKey(): String` (identity used to cache), `limit()`, and `loadTop()`. +- **`AdditiveFeedFilter.kt`** — `abstract class AdditiveFeedFilter : FeedFilter(), IAdditiveFeedFilter`. Adds incremental updates (the "additive" part): `updateListWith(oldList, newItems)` runs `applyFilter(newItems)` and grafts accepted items onto the existing list (re-`sort` + `take(limit())`) without recomputing everything. +- **`ChangesFlowFilter.kt`** — wraps a filter with a coarse "state changed" signal so the ViewModel knows to re-query. +- **`FeedContentState.kt` / `FeedState.kt`** — the reactive state the UI collects. + +### Android DAL (additions on top) `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/`: -- **`FeedFilters.kt`** — `abstract class FeedFilter`. Has `feed(): List` (the sync query against `LocalCache`) and `feedKey(): String` (identity used to cache). -- **`AdditiveComplexFeedFilter.kt`** — `abstract class AdditiveComplexFeedFilter : FeedFilter()`. Adds incremental updates (the "additive" part): when a single new event arrives, the filter can decide whether to graft it onto the existing list without recomputing everything. -- **`ChangesFlowFilter.kt`** — wraps a filter with a coarse "Account state changed" signal so the ViewModel knows to re-query. -- **`FilterByListParams.kt`** — common parameters (author set, exclude muted, limit, since/until) shared across many filters. -- **`DefaultFeedOrder.kt`** — standard sort (by `createdAt` desc, plus tiebreakers for stable paging). +- **`AdditiveComplexFeedFilter.kt`** — `abstract class AdditiveComplexFeedFilter : FeedFilter()`: like `AdditiveFeedFilter` but the incoming items (`Set`) are a different type than the list rows (`T`). +- **`FilterByListParams.kt`** — common parameters (top-nav filter, exclude muted, since/until) shared across many filters. +- **`DefaultFeedOrder.kt`** — standard comparators (`createdAt` desc + id tiebreaker for stable paging) for `Note`, `Event`, and `Card`. +- **`FeedFilters.kt` / `ChangesFlowFilter.kt`** — back-compat typealiases re-exporting the commons classes; don't add logic here. -Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, etc.) live in feature subfolders under `amethyst/.../ui/screen/loggedIn/*/` — each extends `FeedFilter` or `AdditiveComplexFeedFilter`. +Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, etc.) live in feature `dal/` subfolders under `amethyst/.../ui/screen/loggedIn/*/` — each extends `FeedFilter`, `AdditiveFeedFilter`, or `AdditiveComplexFeedFilter`. Desktop has its own in `desktopApp/.../feeds/DesktopFeedFilters.kt`. ## Adding a New Feed -1. **Define the filter.** Extend `AdditiveComplexFeedFilter>` (or plain `FeedFilter` if additivity doesn't matter). Implement: +1. **Define the filter.** Extend `AdditiveFeedFilter` (or plain `FeedFilter` if additivity doesn't matter; `AdditiveComplexFeedFilter` if incoming items differ in type from list rows). Implement: - `feedKey()` — stable identity (e.g. hashtag name, account pubkey). - `feed()` — synchronous scan over `LocalCache` / `Account` state producing an ordered list. - `limit()` — pagination hint. - - If using `AdditiveComplexFeedFilter`: `applyFilter(collection: Set): Set` and `sort(collection: Set): List`. + - If additive: `applyFilter(collection: Set): Set` and `sort(collection: Set): List`. 2. **Pick or write a ViewModel.** If the feed's membership shifts often (bookmarks, notifications), extend `ListChangeFeedViewModel`. Otherwise `FeedViewModel`. 3. **Wire invalidation.** The ViewModel must observe the right `Account` flows + `LocalCacheFlow` so it re-queries when state changes. 4. **Render.** In the composable, collect `viewModel.feedState.feedContent` and render with a `LazyColumn { items(..., key = { it.id }) { NoteCompose(it) } }`. @@ -86,9 +100,9 @@ Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, ## Filter Sharing (Android vs Desktop) -- `FeedFilter` and the concrete filters currently live in `amethyst/.../ui/dal/` — **Android-only**. Desktop has parallel filters in `desktopApp/.../feeds/`. -- ViewModels are in `commons/commonMain/` — **shared**. That's the boundary: filter is Android (could be extracted), ViewModel is shared. -- When porting a new feed, extract the filter to a KMP-friendly location only if both platforms need it. +- The filter **base classes** (`FeedFilter`, `AdditiveFeedFilter`, `ChangesFlowFilter`) and feed state (`FeedContentState`) are in `commons/.../ui/feeds/` — **shared**. ViewModels are in `commons/.../viewmodels/` — **shared**. +- The **concrete** filters are platform-local: Android's in `amethyst/.../ui/screen/loggedIn/*/dal/`, Desktop's in `desktopApp/.../feeds/`. `amethyst/.../ui/dal/` keeps Android-only helpers (`AdditiveComplexFeedFilter`, `FilterByListParams`, `DefaultFeedOrder`) plus back-compat typealiases. +- When porting a feed, share the concrete filter only if both platforms need identical inclusion rules. ## Gotchas @@ -96,7 +110,7 @@ Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, - **`feedKey()` is used as a cache key.** Two different semantic feeds must produce different keys, otherwise their state cross-contaminates. - **Additive updates must stay consistent with the full recompute.** If `applyFilter` accepts a note that `feed()` wouldn't include, UX drifts. - **Paging isn't free** — use `limit()` and `since/until` in `FilterByListParams` rather than trimming a giant scan. -- **Notifications feed is special** — it inspects `Account.followListFlow` and `LocalCache` deletions to hide muted/deleted content; always run through `FilterByListParams.exclude*` paths rather than filtering post-hoc. +- **Notifications feed is special** — it inspects the follow/mute state (`account.kind3FollowList.flow`, `account.hiddenUsers`) and `LocalCache` deletions to hide muted/deleted content; always run through the `FilterByListParams` exclusion paths rather than filtering post-hoc. ## References diff --git a/.claude/skills/feed-patterns/references/feed-filter-composition.md b/.claude/skills/feed-patterns/references/feed-filter-composition.md index 3f345b1b16..4c00329919 100644 --- a/.claude/skills/feed-patterns/references/feed-filter-composition.md +++ b/.claude/skills/feed-patterns/references/feed-filter-composition.md @@ -7,11 +7,12 @@ Step-by-step recipe for composing a new feed. Assume the feed shows `Note`s filt | If… | Use | |-----|-----| | Membership is stable (e.g. "my follows") and you re-compute on change | `FeedFilter` | -| New notes arrive one at a time and should slot into the list incrementally | `AdditiveComplexFeedFilter>` | +| New notes arrive one at a time and should slot into the list incrementally | `AdditiveFeedFilter` | +| Incoming items are a different type than the list rows | `AdditiveComplexFeedFilter` (Android-only) | | The feed is a simple list that changes frequently (e.g. bookmarks, lists) | `FeedFilter` + `ListChangeFeedViewModel` | | The feed is a DM thread | `ChatroomFeedViewModel` (already provides filter machinery) | -All live in `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/`. +The bases live in `commons/src/commonMain/.../commons/ui/feeds/`; `AdditiveComplexFeedFilter` and the `FilterByListParams` / `DefaultFeedOrder` helpers in `amethyst/src/main/java/.../ui/dal/`. ## 2. Write the Filter @@ -19,7 +20,7 @@ All live in `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/`. class HashtagFeedFilter( private val accountViewModel: AccountViewModel, private val hashtag: String, -) : AdditiveComplexFeedFilter>() { +) : AdditiveFeedFilter() { override fun feedKey(): String = "Hashtag-$hashtag" @@ -28,7 +29,7 @@ class HashtagFeedFilter( override fun feed(): List { val params = FilterByListParams.create( excludeMuted = true, - hiddenUsers = accountViewModel.hiddenUsersFlow.value, + hiddenUsers = account.hiddenUsers.flow.value, ) return LocalCache.hashtagIndex[hashtag] .orEmpty() @@ -69,7 +70,7 @@ class HashtagFeedViewModel( ) ``` -If membership changes aggressively (e.g. the user toggles a mute), use `ListChangeFeedViewModel` instead and hook into `Account.muteListFlow`. +If membership changes aggressively (e.g. the user toggles a mute), use `ListChangeFeedViewModel` instead and hook into `account.muteList.flow`. ## 4. Wire Invalidation @@ -78,7 +79,7 @@ If membership changes aggressively (e.g. the user toggles a mute), use `ListChan ```kotlin init { viewModelScope.launch { - accountViewModel.muteListFlow.collect { invalidateAll() } + account.muteList.flow.collect { invalidateAll() } } } ``` diff --git a/.claude/skills/gradle-expert/SKILL.md b/.claude/skills/gradle-expert/SKILL.md index 208caa11cc..04f67cf403 100644 --- a/.claude/skills/gradle-expert/SKILL.md +++ b/.claude/skills/gradle-expert/SKILL.md @@ -5,11 +5,11 @@ description: Build optimization, dependency resolution, and multi-module KMP tro # Gradle Expert -Build system expertise for AmethystMultiplatform's 4-module KMP architecture. Focus: practical troubleshooting, dependency resolution, and project-specific optimizations. +Build system expertise for AmethystMultiplatform's 10-module KMP architecture (`amethyst`, `benchmark`, `quartz`, `geode`, `commons`, `quic`, `nestsClient`, `desktopApp`, `cli`, `quic-interop` — see `settings.gradle.kts`). Focus: practical troubleshooting, dependency resolution, and project-specific optimizations. ## Build Architecture Mental Model -Think of this project as **4 layers**: +The core app stack is **4 layers** (the other modules hang off it: `cli` and `geode` are JVM apps over `commons`/`quartz`, `nestsClient` sits on `quic`, `benchmark` and `quic-interop` are test harnesses): ``` ┌─────────────┬─────────────┐ @@ -165,11 +165,11 @@ implementation(libs.jna) **The problem:** Two Compose ecosystems (Multiplatform + AndroidX) must align, or duplicate classes. -**Current project config:** +**Current project config** (always re-check `gradle/libs.versions.toml` — these drift): ```toml -composeMultiplatform = "1.9.3" # Plugin + runtime -composeBom = "2025.12.01" # AndroidX Compose BOM -kotlin = "2.3.0" +composeMultiplatform = "1.11.0" # Plugin + runtime +composeBom = "2026.05.01" # AndroidX Compose BOM +kotlin = "2.3.21" ``` **Rule:** Compose Multiplatform version must be compatible with Kotlin version. Check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html diff --git a/.claude/skills/gradle-expert/references/dependency-graph.md b/.claude/skills/gradle-expert/references/dependency-graph.md index 411f4d0233..8b07e1bced 100644 --- a/.claude/skills/gradle-expert/references/dependency-graph.md +++ b/.claude/skills/gradle-expert/references/dependency-graph.md @@ -3,46 +3,50 @@ ## Visual Hierarchy ``` -┌─────────────────────────────────────────────────────────┐ -│ Root Project │ -│ (Amethyst) │ -└─────────────────────────────────────────────────────────┘ - │ - ┌────────────────┼────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ :amethyst │ │ :desktopApp │ │ :benchmark │ -│ (Android) │ │ (JVM) │ │ (Android) │ -└─────────────┘ └─────────────┘ └─────────────┘ - │ │ │ - │ │ │ - └────────────────┼────────────────┘ - │ - ▼ - ┌─────────────┐ - │ :commons │ - │ (KMP UI) │ - │ │ - │ jvmAndroid │ - │ / \ │ - │ jvm android│ - └─────────────┘ - │ - │ - ▼ - ┌─────────────┐ - │ :quartz │ - │(KMP Library)│ - │ │ - │ commonMain │ - │ │ │ - │ jvmAndroid │ - │ / | \ │ - │jvm and ios │ - └─────────────┘ + Apps / harnesses Libraries +┌─────────────┐ ┌─────────────┐ ┌────────────┐ +│ :amethyst │ │ :desktopApp │ │ :benchmark │ +│ (Android) │ │ (JVM) │ │ (Android) │ +└──┬───┬───┬──┘ └──┬───────┬──┘ └─┬───────┬──┘ + │ │ └───────┼────┐ │ │ │ + │ │ │ │ │ │ │ + │ ▼ ▼ │ │ ▼ │ + │ ┌────────────────┐ │ │ (androidTest │ + │ │ :commons │◄┼──┼──only) │ + │ │ (KMP UI) │ │ │ │ + │ └───────┬────────┘ │ │ │ + │ │ ▲ │ │ │ + ▼ │ │ │ │ │ +┌──────────────┐ │ ┌─┴──┴─┐ ┌───────┐ │ +│ :nestsClient │ │ │ :cli │ │:geode │ │ +│ (KMP, MoQ) │ │ │(JVM) │ │(JVM │ │ +└──┬────────┬──┘ │ └──┬───┘ │relay) │ │ + │ │ │ │ └───┬───┘ │ + ▼ │ │ │ │ │ +┌────────┐ │ │ │ │ │ +│ :quic │ │ │ │ │ │ +│ (KMP) │ │ │ │ │ │ +└───┬────┘ │ │ │ │ │ + │ ▲ │ │ │ │ │ + │ └── :quic-interop │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ + ┌──────────────────────────────┐ + │ :quartz │ + │ (KMP Library) │ + └──────────────────────────────┘ ``` +Verified edges (from each module's `build.gradle.kts`): + +- `:amethyst` → `:quartz`, `:commons`, `:nestsClient` +- `:desktopApp` → `:quartz`, `:commons` +- `:benchmark` → `:quartz`, `:commons` (androidTest only) +- `:cli` → `:quartz`, `:commons` +- `:geode` → `:quartz` (api + testFixtures) +- `:nestsClient` → `:quartz` (api), `:quic` +- `:quic` → `:quartz` (api) +- `:quic-interop` → `:quic` (project dir: `quic/interop`) + ## Module Details ### :quartz (KMP Nostr Library) @@ -86,11 +90,41 @@ **Type:** Android Library **Targets:** Android **Dependencies:** -- Modules: `:commons`, `:quartz` +- Modules: `:commons`, `:quartz` (androidTest only) - External: AndroidX Benchmark **Role:** Performance benchmarking for Android builds +### :cli (Amy CLI) +**Type:** JVM Application (no Compose) +**Dependencies:** `:quartz`, `:commons` + +**Role:** `amy`, the non-interactive command-line client; thin assembly layer, no new logic (see `amy-expert` skill) + +### :geode (Relay Server) +**Type:** JVM Application (Ktor) +**Dependencies:** `:quartz` (api + testFixtures) + +**Role:** Standalone Nostr relay built on quartz's relay-server code + +### :quic (QUIC Transport) +**Type:** Kotlin Multiplatform Library +**Dependencies:** `:quartz` (api) + +**Role:** Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport client (no JNI); transport for MoQ + +### :nestsClient (Audio Rooms) +**Type:** Kotlin Multiplatform Library +**Dependencies:** `:quartz` (api), `:quic` + +**Role:** MoQ / moq-lite audio-room client for the NIP-53 nests feature + +### :quic-interop (Interop Harness) +**Type:** JVM Application (project dir `quic/interop`) +**Dependencies:** `:quic` + +**Role:** QUIC interop-runner test client + ## Dependency Flow Patterns ### Desktop Build Chain @@ -177,9 +211,9 @@ implementation(libs.jna) // JAR variant implementation(compose.ui) // Compose Multiplatform BOM implementation(compose.material3) -// Version catalog alignment -composeMultiplatform = "1.9.3" -composeBom = "2025.12.01" // AndroidX Compose +// Version catalog alignment (re-check libs.versions.toml — these drift) +composeMultiplatform = "1.11.0" +composeBom = "2026.05.01" // AndroidX Compose ``` **Why:** Two Compose ecosystems (Multiplatform + AndroidX) must align diff --git a/.claude/skills/kotlin-expert/SKILL.md b/.claude/skills/kotlin-expert/SKILL.md index bc1d81bc06..619170dd3e 100644 --- a/.claude/skills/kotlin-expert/SKILL.md +++ b/.claude/skills/kotlin-expert/SKILL.md @@ -807,6 +807,5 @@ Passing lambda to function? --- -**Version:** 1.0.0 -**Last Updated:** 2025-12-30 -**Codebase Reference:** AmethystMultiplatform commit 258c4e011 +**Version:** 1.0.1 +**Last Updated:** 2026-06-10 diff --git a/.claude/skills/relay-client/SKILL.md b/.claude/skills/relay-client/SKILL.md index 8aaa258d05..0bdea82309 100644 --- a/.claude/skills/relay-client/SKILL.md +++ b/.claude/skills/relay-client/SKILL.md @@ -24,17 +24,23 @@ relayClient/ ├── assemblers/ # "Given these inputs, build this relay Filter" │ ├── MetadataFilterAssembler.kt # kind 0 for N pubkeys │ ├── ReactionsFilterAssembler.kt # kind 7 for N note ids -│ └── FeedMetadataCoordinator.kt # coordinates metadata loads for a feed +│ ├── FeedMetadataCoordinator.kt # coordinates metadata loads for a feed +│ └── CashuMintDirectoryFilterAssembler.kt / CashuWalletFilterAssembler.kt ├── composeSubscriptionManagers/ │ ├── ComposeSubscriptionManager.kt # interface Subscribable │ ├── MutableComposeSubscriptionManager.kt # reference impl │ └── ComposeSubscriptionManagerControls.kt # DisposableEffect-style controls ├── eoseManagers/ # EOSE tracking per subscription +│ └── IEoseManager / BaseEoseManager / PerKeyEoseManager / SingleSubEoseManager +├── nip17Dm/ # gift-wrap DM plumbing +│ └── FilterGiftWrapsToPubkey.kt / GiftWrapDecryptor.kt ├── preload/ │ ├── MetadataPreloader.kt # bulk-fetch metadata with rate limiting │ └── MetadataRateLimiter.kt # token-bucket-ish limiter └── subscriptions/ - └── KeyDataSourceSubscription.kt # "this set of keys drives this filter" + ├── KeyDataSourceSubscription.kt # "this set of keys drives this filter" + ├── LifecycleAwareKeyDataSourceSubscription.kt + └── PrioritizedSubscriptionQueue.kt / SubscriptionPriority.kt ``` ## Core Concept: `Subscribable`