Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20). - Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references. - Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching. - Removes compose-desktop.md (superseded by desktop-expert/). - Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption, large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing, android-expert/image-loading. - New skills: account-state (Account + LocalCache), relay-client (subscriptions, filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family), auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Account StateFlow 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.
|
||||
|
||||
(Flow names are exact as of the current `Account.kt`; if a flow has been renamed, grep `Account.kt` for the old 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/` |
|
||||
|
||||
## Relays & Connectivity
|
||||
|
||||
| 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` |
|
||||
|
||||
## 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/` |
|
||||
|
||||
## Messaging
|
||||
|
||||
| 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/` |
|
||||
|
||||
## 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/` |
|
||||
|
||||
## Publishing Mutations
|
||||
|
||||
Every flow has a corresponding mutation method on `Account` that:
|
||||
|
||||
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.
|
||||
|
||||
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(...)`
|
||||
|
||||
## 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`).
|
||||
@@ -0,0 +1,101 @@
|
||||
# LocalCache: The Singleton Event Store
|
||||
|
||||
`amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt` is the singleton (`object LocalCache`) that holds every event the client has received during the session. All rendering, all feed building, all search goes through it.
|
||||
|
||||
## Shape
|
||||
|
||||
```kotlin
|
||||
object LocalCache : ILocalCache, ICacheProvider {
|
||||
val notes: LargeCache<HexKey, Note>
|
||||
val users: LargeCache<HexKey, User>
|
||||
val addressables: LargeCache<Address, Note>
|
||||
val channels, deletionIndex, hashtagIndex, …
|
||||
}
|
||||
```
|
||||
|
||||
All `LargeCache<K,V>` — see `nostr-expert/references/large-cache.md`. Thread-safe `getOrCreate`, functional scan with `forEach` / `filter` / `map`.
|
||||
|
||||
## Insertion Path
|
||||
|
||||
```
|
||||
Relay frame (EVENT "sub-id" {...})
|
||||
│
|
||||
▼
|
||||
RelayPool / subscription manager calls parseNostrEvent(json)
|
||||
│
|
||||
▼
|
||||
EventFactory.create(kind, ...) → typed Event subclass
|
||||
│
|
||||
▼
|
||||
LocalCache.consume(event) / insertOrUpdateNote(event)
|
||||
│
|
||||
├─ notes.getOrCreate(id) { Note(id) } — finds/creates the Note wrapper
|
||||
├─ updates note.event if this is a newer replaceable / first time for regular
|
||||
├─ reindex: hashtag tags → hashtagIndex, addressable → addressables, deletions → deletionIndex
|
||||
├─ for metadata: user.latestMetadata = event; user.liveMetadata.tryEmit(user)
|
||||
└─ LocalCacheFlow signals listeners that something changed
|
||||
```
|
||||
|
||||
`Note` and `User` are mutable wrappers — `getOrCreate` returns the same object across subsequent inserts for the same id/pubkey, which is why other code can `remember(noteId)` a `Note` reference and have it stay fresh.
|
||||
|
||||
## Lookup
|
||||
|
||||
```kotlin
|
||||
// By id (regular or replaceable)
|
||||
val note: Note = LocalCache.getOrCreateNote(id)
|
||||
|
||||
// By `kind:pubkey:d-tag`
|
||||
val addressable: Note? = LocalCache.getAddressableNoteIfExists(address)
|
||||
|
||||
// By pubkey
|
||||
val user: User = LocalCache.getOrCreateUser(pubKey)
|
||||
|
||||
// By hashtag
|
||||
LocalCache.hashtagIndex.filter { _, notes -> ... }
|
||||
```
|
||||
|
||||
All `getOrCreate*` functions are safe to call from any thread. They return immediately; they do NOT trigger network I/O.
|
||||
|
||||
## Eviction
|
||||
|
||||
Android-only. `amethyst/.../service/eventCache/MemoryTrimmingService.kt` listens for `ComponentCallbacks2.onTrimMemory` levels and drops least-recently-used entries from `notes` and `users`. On aggressive eviction, previously-returned `Note` / `User` references remain usable (they're just detached from the cache) but any new ids will produce new objects.
|
||||
|
||||
## Reactive Consumption
|
||||
|
||||
### Note-level
|
||||
|
||||
```kotlin
|
||||
val note = LocalCache.getOrCreateNote(id)
|
||||
val metadata by note.flowSet.metadata.collectAsState()
|
||||
// `flowSet` has flows for: metadata, replies, reactions, zaps, reports, …
|
||||
```
|
||||
|
||||
### Global
|
||||
|
||||
```kotlin
|
||||
LocalCacheFlow.live.collectLatest {
|
||||
// coarse "something changed" ping — used by feeds to re-run filters
|
||||
}
|
||||
```
|
||||
|
||||
For per-feature reactivity (follow list changed, relays changed), prefer `Account.<featureFlow>` over `LocalCacheFlow`.
|
||||
|
||||
## Deletion / Replacement
|
||||
|
||||
- **Regular events**: once inserted, the first event wins unless explicitly deleted via a kind-5 deletion. `deletionIndex` tracks ids to hide.
|
||||
- **Replaceable** (kinds 0, 3, 10000-19999): a newer `created_at` replaces the older event in-place on the same `Note` wrapper.
|
||||
- **Addressable** (kinds 30000-39999): same as replaceable but keyed by `kind:pubkey:d-tag` in the `addressables` index.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Don't hold a direct `Event` reference** — hold the `Note` wrapper. The `Note.event` field can be replaced by newer replaceable/addressable events behind your back.
|
||||
- **`LocalCache` is process-global**. Tests must either use a dedicated test fixture or reset it between cases.
|
||||
- **No TTL beyond memory pressure.** A long-running session accumulates. If you need bounded retention, do it at the feed / filter layer.
|
||||
- **Scanning the full cache is expensive** in hot paths. Always prefer an index (hashtag, addressable) or a pre-built feed filter.
|
||||
- **Eviction is not atomic with in-flight coroutines**. If you `forEach` during low-memory, you may see concurrent removals — that's fine, the snapshot semantics in `LargeCache` keep it safe, but your result set shrinks.
|
||||
|
||||
## Related
|
||||
|
||||
- `nostr-expert/references/large-cache.md` — the underlying cache primitive.
|
||||
- `nostr-expert/references/event-factory.md` — how raw JSON becomes the typed `Event` that `LocalCache` stores.
|
||||
- `feed-patterns` skill — how feeds scan and observe `LocalCache` efficiently.
|
||||
Reference in New Issue
Block a user