60edd473c7
- 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).
7.9 KiB
7.9 KiB
name, description
| name | description |
|---|---|
| feed-patterns | 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. |
Feed Patterns
Amethyst's "feed" abstraction is: a FeedFilter that decides which notes belong in a list, plus a FeedViewModel that exposes the current state reactively to the UI. Every scrollable list — home, profile, hashtag, bookmarks, notifications, DMs — is a variant of this.
When to Use This Skill
- Adding a new screen that shows a list of notes.
- Modifying an existing feed's filtering / ordering / inclusion rules.
- Investigating why a feed doesn't update after a mute/follow/bookmark change.
- Deciding whether to extend a ViewModel or write a new filter.
- Understanding the Android ⇄ Desktop sharing boundary for feeds.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ commons/.../viewmodels/ (shared, KMP) │
│ FeedViewModel ◄── ListChangeFeedViewModel │
│ ◄── ChatroomFeedViewModel │
│ ◄── MarmotGroupFeedViewModel │
│ │
│ FeedContentState — the flow the UI collects │
└─────────────────────────────────────────────────────────────┘
▲
│ uses
│
┌─────────────────────────────────────────────────────────────┐
│ amethyst/.../ui/dal/ (Android; feeds defined per screen) │
│ FeedFilter<T> (abstract) │
│ AdditiveComplexFeedFilter<T, U> │
│ ChangesFlowFilter │
│ FilterByListParams │
│ DefaultFeedOrder │
│ │
│ Plus concrete feeds: HomeFeedFilter, HashtagFeedFilter, │
│ BookmarkListFeedFilter, NotificationFeedFilter, … │
└─────────────────────────────────────────────────────────────┘
▲
│ reads
│
┌─────────────────────────────────────────────────────────────┐
│ model/LocalCache.kt + Account.<featureFlow> │
└─────────────────────────────────────────────────────────────┘
Key Files
Shared (commons)
commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/:
FeedViewModel.kt—abstract class FeedViewModel(localFilter, cacheProvider). Holds aFeedContentState, subscribes to invalidation signals (fromAccountflows andLocalCacheFlow), re-runs the filter, and emits a newFeedStatefor the UI.ListChangeFeedViewModel.kt— specialization for feeds whose membership changes frequently (e.g. bookmarks).ChatroomFeedViewModel.kt— DM thread feed.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)
amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/:
FeedFilters.kt—abstract class FeedFilter<T>. Hasfeed(): List<T>(the sync query againstLocalCache) andfeedKey(): 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 (bycreatedAtdesc, plus tiebreakers for stable paging).
Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, etc.) live in feature subfolders under amethyst/.../ui/screen/loggedIn/*/ — each extends FeedFilter or AdditiveComplexFeedFilter.
Adding a New Feed
- Define the filter. Extend
AdditiveComplexFeedFilter<Note, Set<HexKey>>(or plainFeedFilter<Note>if additivity doesn't matter). Implement:feedKey()— stable identity (e.g. hashtag name, account pubkey).feed()— synchronous scan overLocalCache/Accountstate producing an ordered list.limit()— pagination hint.- If using
AdditiveComplexFeedFilter:applyFilter(collection: Set<Note>): Set<Note>andsort(collection: Set<Note>): List<Note>.
- Pick or write a ViewModel. If the feed's membership shifts often (bookmarks, notifications), extend
ListChangeFeedViewModel. OtherwiseFeedViewModel. - Wire invalidation. The ViewModel must observe the right
Accountflows +LocalCacheFlowso it re-queries when state changes. - Render. In the composable, collect
viewModel.feedState.feedContentand render with aLazyColumn { items(..., key = { it.id }) { NoteCompose(it) } }. - Subscribe to relays. Most feeds also need a
Subscribableto fetch historical events. See therelay-clientskill.
Filter Sharing (Android vs Desktop)
FeedFilterand the concrete filters currently live inamethyst/.../ui/dal/— Android-only. Desktop has parallel filters indesktopApp/.../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.
Gotchas
- Never scan
LocalCachefrom a composable. Always go through aFeedFilter+FeedViewModel, which does it on a background dispatcher and debounces invalidation. 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
applyFilteraccepts a note thatfeed()wouldn't include, UX drifts. - Paging isn't free — use
limit()andsince/untilinFilterByListParamsrather than trimming a giant scan. - Notifications feed is special — it inspects
Account.followListFlowandLocalCachedeletions to hide muted/deleted content; always run throughFilterByListParams.exclude*paths rather than filtering post-hoc.
References
references/feed-filter-composition.md— step-by-step for adding a feed.references/viewmodel-base-classes.md— inheritance graph for theFeedViewModelfamily.- Complements:
account-state(where the data lives),relay-client(how to subscribe),compose-expert(how to render).