---
name: account-state
description: Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow), FeedFilter/AdditiveFeedFilter, FeedViewModel. Use when adding an account setting or list, a feed, or choosing LocalCache vs account flows.
---

# Account, LocalCache and Feeds

The client state backbone, all headless in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/`
(abbreviated `commons/…/` below):

- `model/cache/LocalCache.kt` — `object LocalCache : EventCache()`. Every `Note` / `User` the client has
  seen. All logic is in `model/cache/EventCache.kt` (an open class so tests can build their own);
  Android and Desktop both use the `LocalCache` singleton (`AppModules.kt`, `DesktopAppModules.kt`).
- `model/Account.kt` — one `Account` per signed-in user: ~50 per-feature **state objects**
  (`kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, …), each exposing `.flow`.
- `feeds/` + `viewmodels/` + `<feature>/dal/` — feeds: a `FeedFilter` scans `LocalCache`, a
  `FeedContentState` / `FeedViewModel` keeps the result live.

```
relay EVENT ─► EventCache.justConsume ─► Note/User updated, indexes, live.newNote(...)
                     │                                   │
                     ▼                                   ▼
   XState pins its addressable note,            LocalCacheFlow.newEventBundles
   note.flow().metadata → XState.flow           → FeedViewModel → FeedFilter.applyFilter
                     │                                   │
                     └────────► ViewModels / composables ◄┘
```

## Account state objects

Pattern, copied from `model/nip51Lists/muteList/MuteListState.kt`:

- **Pin the note.** `val muteListNote = cache.getOrCreateAddressableNote(MuteListEvent.createAddress(signer.pubKey))`.
  `LocalCache`'s `notes` / `addressables` are `LargeSoftCache` (weak references), so the state
  object's field is what keeps its note from being collected.
- **Expose one `StateFlow`.** Map `muteListNote.flow().metadata.stateFlow` → decrypt (private lists go
  through a per-feature `…DecryptionCache` such as `MuteListDecryptionCache`, held next to it in
  `Account` as `muteListDecryptionCache`) → fall back to the `AccountSettings` backup
  (`settings.backupMuteList`) when the event is absent → `stateIn(scope, SharingStarted.Eagerly, …)`.
- **Mutation helpers return a signed event — publishing it is the caller's job.**
  `MuteListState.hideUser(pubkey)` returns a `MuteListEvent`; `Account.hideUser` does
  `sendMyPublicAndPrivateOutbox(muteList.hideUser(pubkeyHex))`. A state change you never send is
  invisible to every other client and is lost when the next relay copy arrives.
- Consumers read `account.<feature>.flow`, never a `MutableStateFlow` on `Account`.

Full catalog of properties, classes, kinds and packages: `references/account-state-flow.md`.

### Adding an account-scoped setting or list

1. Persisted as a Nostr event? Pick the kind (NIP-51 list, NIP-78 app data, a relay list) and make
   sure quartz has the event class (see quartz's `nostr-expert` skill; a new one is a quartz
   change, built here with `-PquartzDir=../quartz`).
2. Add `commons/…/model/nipXX…/XState.kt` modeled on `MuteListState` (encrypted) or
   `nip51Lists/BookmarkListState.kt` (plain): pin, flow, suspend mutation helpers that build the
   event with the quartz class and sign it.
3. Instantiate it as a `val` in `Account.kt` (plus its `DecryptionCache` if private) and add an
   `Account` method that sends the returned event (`sendMyPublicAndPrivateOutbox`, `sendAutomatic`, …).
   If the list must survive relay loss, add a `backupX` field to `AccountSettings`.
4. Make sure `EventCache.justConsumeInnerInner` lists the event class — a kind that is not listed
   falls into the `else` branch and is **rejected**, so the state object never sees it, even though
   `EventFactory` parses it. Replaceable kinds without custom logic go in the
   `consumeBaseReplaceable` group.
5. Relay subscription: `relay-client`. UI: settings screens are shared in
   `commonsUI/…/commons/ui/screen/loggedIn/settings/`; strings follow the AGENTS.md *Strings* rules.

## LocalCache vs Account flow vs feed

| You render… | Read |
|---|---|
| one note / user you have an id for | `LocalCache.getOrCreateNote(id)` / `getOrCreateUser(pubkey)`, then `note.flow().metadata` (or `replies`, `reactions`, `zaps`, …) |
| "my follows / mutes / relays / bookmarks" | `account.kind3FollowList.flow`, `account.muteList.flow`, `account.nip65RelayList.flow`, `account.hiddenUsers.flow`, … |
| a list of notes | a `FeedFilter` + `FeedContentState` / `FeedViewModel` — `references/feeds.md` |
| ad-hoc filter results | `LocalCache.observeNotes(filter: Filter): Flow<List<Note>>` |

Never scan `LocalCache.notes` inside a composable.

## Feeds in one paragraph

Bases are in `commons/…/feeds/`: `IFeedFilter` / `FeedFilter<T>` (`feed()`, `feedKey()`, `limit()`
default 500, `showHiddenKey()`), `AdditiveFeedFilter<T>` (adds `applyFilter` + `sort` for
incremental merges), `AdditiveComplexFeedFilter<T, U>` (incoming type differs from row type, e.g.
`home/dal/HomeLiveFilter.kt`), `ChangesFlowFilter<T>`, `FilterByListParams`, `DefaultFeedOrder` /
`sortedByDefaultFeedOrder()`, `FeedContentState`, `FeedState`. Concrete filters are **shared**, in
`commons/…/<feature>/dal/` (`home/dal/`, `hashtag/dal/`, `notifications/dal/`, ~55 of them). The
account's main-tab feeds are long-lived `FeedContentState`s in `viewmodels/AccountFeedContentStates.kt`
(owned by `BaseAccountViewModel.feedStates`); per-screen feeds use a `FeedViewModel` subclass. Recipe,
real code and traps: `references/feeds.md`.

## Gotchas

- **`LocalCache` is shared across accounts.** Switching accounts doesn't wipe it; every `Account`
  derives its flows from the same cache. Filters that depend on the account must put the account
  pubkey in `feedKey()`.
- **Hold the `Note`, not the `Event`.** `getOrCreate*` returns the same wrapper per id; for
  replaceable/addressable notes `note.event` is swapped in place when a newer version arrives.
- **Flows on a `Note` are not durable.** `CachePruner` (`model/cache/CachePruner.kt`) calls
  `clearFlow()` on notes during cleanup, and anything not strongly held can be collected. Re-fetch
  via `note.flow()`; don't stash flow references.
- **Observers can fire before derived state exists.** `EventCache` notifies observers
  (`refreshNewNoteObservers`) as part of consuming an event, before some of what it derives from it
  (channels, indexes) is filled in. Re-read state in the collector rather than trusting it is complete.
- **Memory pressure trims.** `MemoryTrimmingService` (`model/cache/`) is run by both app modules,
  and `AccountFeedContentStates` trims every feed to 200 items on `memoryPressure`. Never assume a
  previously seen note is still resident.

## References

- `references/account-state-flow.md` — catalog of `Account` state objects, kinds and packages.
- `references/local-cache.md` — `EventCache` structures, insertion path, lookups, pruning.
- `references/feeds.md` — adding a feed: filter, state holder, invalidation, rendering, tests.
- Related skills: quartz's `nostr-expert` (event classes, `EventFactory`), `relay-client` (subscriptions),
  `auth-signers` (how mutations sign), `compose-expert` (rendering).


---
Source attribution: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/account-state/SKILL.md
License: MIT
License information: https://opensource.org/license/mit
Original upstream instructions; downloaded from Mega AI Hub.

## Upstream license notice

MIT License

Copyright (c) 2023 Vitor Pamplona

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



# Mega AI Hub resource metadata

Creator / publisher: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/account-state/SKILL.md
Attribution basis: Source repository record

Fields of work: Development & Engineering, Product Management
Roles: Backend Developer, Mobile Developer, Product Manager
Specialties: Backend
Tools: Amethyst
Country applicability: Country not specified
Classification basis: Source catalog labels
Instruction SHA-256: a851932bbf827d7889ca63e72377e8a004160ef0ff035271a11e658e01627076
