mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-30 19:46:17 +00:00
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
This commit is contained in:
+5
-2
@@ -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)
|
||||
|
||||
@@ -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/`
|
||||
|
||||
@@ -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/
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<T>`.
|
||||
- Expose a `StateFlow<T>` 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.<featureFlow>`.
|
||||
- **Are you rendering "my follows", "my mutes", "my relays"?** → `account.<feature>.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).
|
||||
|
||||
@@ -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.<property>.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<T> = _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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T> (abstract base) │
|
||||
│ IAdditiveFeedFilter / AdditiveFeedFilter<T> │
|
||||
│ ChangesFlowFilter │
|
||||
│ FeedContentState, FeedState — the flow the UI collects │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ uses
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ amethyst/.../ui/dal/ (Android; feeds defined per screen) │
|
||||
│ FeedFilter<T> (abstract) │
|
||||
│ amethyst/.../ui/dal/ (Android-only additions) │
|
||||
│ AdditiveComplexFeedFilter<T, U> │
|
||||
│ 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.<featureFlow> │
|
||||
│ model/LocalCache.kt + account.<feature>.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<T> : IFeedFilter<T>`. Has `feed(): List<T>` (the sync query against the cache), `feedKey(): String` (identity used to cache), `limit()`, and `loadTop()`.
|
||||
- **`AdditiveFeedFilter.kt`** — `abstract class AdditiveFeedFilter<T> : FeedFilter<T>(), IAdditiveFeedFilter<T>`. 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<T>`. Has `feed(): List<T>` (the sync query against `LocalCache`) and `feedKey(): String` (identity used to cache).
|
||||
- **`AdditiveComplexFeedFilter.kt`** — `abstract class AdditiveComplexFeedFilter<T, U> : FeedFilter<T>()`. 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<T, U> : FeedFilter<T>()`: like `AdditiveFeedFilter` but the incoming items (`Set<U>`) 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<Note, Set<HexKey>>` (or plain `FeedFilter<Note>` if additivity doesn't matter). Implement:
|
||||
1. **Define the filter.** Extend `AdditiveFeedFilter<Note>` (or plain `FeedFilter<Note>` if additivity doesn't matter; `AdditiveComplexFeedFilter<T, U>` 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<Note>): Set<Note>` and `sort(collection: Set<Note>): List<Note>`.
|
||||
- If additive: `applyFilter(collection: Set<Note>): Set<Note>` and `sort(collection: Set<Note>): List<Note>`.
|
||||
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
|
||||
|
||||
|
||||
@@ -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<Note>` |
|
||||
| New notes arrive one at a time and should slot into the list incrementally | `AdditiveComplexFeedFilter<Note, Set<Note>>` |
|
||||
| New notes arrive one at a time and should slot into the list incrementally | `AdditiveFeedFilter<Note>` |
|
||||
| Incoming items are a different type than the list rows | `AdditiveComplexFeedFilter<T, U>` (Android-only) |
|
||||
| The feed is a simple list that changes frequently (e.g. bookmarks, lists) | `FeedFilter<Note>` + `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<Note, Set<Note>>() {
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
override fun feedKey(): String = "Hashtag-$hashtag"
|
||||
|
||||
@@ -28,7 +29,7 @@ class HashtagFeedFilter(
|
||||
override fun feed(): List<Note> {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T>
|
||||
│ ├── 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<T>`
|
||||
|
||||
Reference in New Issue
Block a user