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 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
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 │
│ │
│ │
│ 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-only additions) │
│ AdditiveComplexFeedFilter<T, U> │
│ FilterByListParams │
│ DefaultFeedOrder (Note/Event/Card comparators) │
│ (FeedFilters.kt & ChangesFlowFilter.kt here are just │
│ back-compat typealiases re-exporting commons) │
│ │
│ Concrete feeds: HomeNewThreadFeedFilter, │
│ HashtagFeedFilter, NotificationFeedFilter, … live in │
│ feature folders under ui/screen/loggedIn/*/dal/ │
└─────────────────────────────────────────────────────────────┘
▲
│ reads
│
┌─────────────────────────────────────────────────────────────┐
│ model/LocalCache.kt + account.<feature>.flow │
└─────────────────────────────────────────────────────────────┘
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.
Shared filter bases (commons)
commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/:
FeedFilter.kt—abstract class FeedFilter<T> : IFeedFilter<T>. Hasfeed(): List<T>(the sync query against the cache),feedKey(): String(identity used to cache),limit(), andloadTop().AdditiveFeedFilter.kt—abstract class AdditiveFeedFilter<T> : FeedFilter<T>(), IAdditiveFeedFilter<T>. Adds incremental updates (the "additive" part):updateListWith(oldList, newItems)runsapplyFilter(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/:
AdditiveComplexFeedFilter.kt—abstract class AdditiveComplexFeedFilter<T, U> : FeedFilter<T>(): likeAdditiveFeedFilterbut 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 (createdAtdesc + id tiebreaker for stable paging) forNote,Event, andCard.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 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
- Define the filter. Extend
AdditiveFeedFilter<Note>(or plainFeedFilter<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 overLocalCache/Accountstate producing an ordered list.limit()— pagination hint.- If additive:
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)
- The filter base classes (
FeedFilter,AdditiveFeedFilter,ChangesFlowFilter) and feed state (FeedContentState) are incommons/.../ui/feeds/— shared. ViewModels are incommons/.../viewmodels/— shared. - The concrete filters are platform-local: Android's in
amethyst/.../ui/screen/loggedIn/*/dal/, Desktop's indesktopApp/.../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
- 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 the follow/mute state (
account.kind3FollowList.flow,account.hiddenUsers) andLocalCachedeletions to hide muted/deleted content; always run through theFilterByListParamsexclusion 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).