fix: give each Feeds top nav filter its own Account + LocalPreferences flow

- Follow Packs was piggybacking on the Discovery top nav filter; give it a
  dedicated defaultFollowPacksFollowList StateFlow, liveFollowPacksFollowLists,
  and a DEFAULT_FOLLOW_PACKS_FOLLOW_LIST preference key so its top nav filter
  no longer mutates Discovery (and vice versa).
- Persist Communities, Badges, Browse Emoji Sets, and Follow Packs top nav
  filters in encrypted SharedPreferences so they survive app restart.
- Longs top nav was restricted to kind3GlobalPeople; switch it to
  kind3GlobalPeopleRoutes to match the other media/reading feeds and expose
  hashtag/geohash/relay/interest-set filters.
This commit is contained in:
Claude
2026-04-21 23:14:05 +00:00
parent 4f82b68d06
commit e2acacc7a1
9 changed files with 52 additions and 7 deletions
@@ -107,6 +107,10 @@ private object PrefKeys {
const val DEFAULT_LIVE_STREAMS_FOLLOW_LIST = "defaultLiveStreamsFollowList" const val DEFAULT_LIVE_STREAMS_FOLLOW_LIST = "defaultLiveStreamsFollowList"
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList" const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
const val DEFAULT_ARTICLES_FOLLOW_LIST = "defaultArticlesFollowList" const val DEFAULT_ARTICLES_FOLLOW_LIST = "defaultArticlesFollowList"
const val DEFAULT_BADGES_FOLLOW_LIST = "defaultBadgesFollowList"
const val DEFAULT_BROWSE_EMOJI_SETS_FOLLOW_LIST = "defaultBrowseEmojiSetsFollowList"
const val DEFAULT_COMMUNITIES_FOLLOW_LIST = "defaultCommunitiesFollowList"
const val DEFAULT_FOLLOW_PACKS_FOLLOW_LIST = "defaultFollowPacksFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" // legacy, kept for migration const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" // legacy, kept for migration
const val NWC_WALLETS = "nwcWallets" const val NWC_WALLETS = "nwcWallets"
const val DEFAULT_NWC_WALLET_ID = "defaultNwcWalletId" const val DEFAULT_NWC_WALLET_ID = "defaultNwcWalletId"
@@ -354,6 +358,10 @@ object LocalPreferences {
putString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLiveStreamsFollowList.value)) putString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLiveStreamsFollowList.value))
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value)) putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
putString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultArticlesFollowList.value)) putString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultArticlesFollowList.value))
putString(PrefKeys.DEFAULT_BADGES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultBadgesFollowList.value))
putString(PrefKeys.DEFAULT_BROWSE_EMOJI_SETS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultBrowseEmojiSetsFollowList.value))
putString(PrefKeys.DEFAULT_COMMUNITIES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultCommunitiesFollowList.value))
putString(PrefKeys.DEFAULT_FOLLOW_PACKS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultFollowPacksFollowList.value))
val walletEntries = settings.nwcWallets.value.mapNotNull { it.denormalize() } val walletEntries = settings.nwcWallets.value.mapNotNull { it.denormalize() }
if (walletEntries.isNotEmpty()) { if (walletEntries.isNotEmpty()) {
@@ -619,6 +627,10 @@ object LocalPreferences {
defaultLiveStreamsFollowList = MutableStateFlow(followListPrefs.liveStreams), defaultLiveStreamsFollowList = MutableStateFlow(followListPrefs.liveStreams),
defaultLongsFollowList = MutableStateFlow(followListPrefs.longs), defaultLongsFollowList = MutableStateFlow(followListPrefs.longs),
defaultArticlesFollowList = MutableStateFlow(followListPrefs.articles), defaultArticlesFollowList = MutableStateFlow(followListPrefs.articles),
defaultBadgesFollowList = MutableStateFlow(followListPrefs.badges),
defaultBrowseEmojiSetsFollowList = MutableStateFlow(followListPrefs.browseEmojiSets),
defaultCommunitiesFollowList = MutableStateFlow(followListPrefs.communities),
defaultFollowPacksFollowList = MutableStateFlow(followListPrefs.followPacks),
nwcWallets = MutableStateFlow(nwcWalletsLoaded.await().first), nwcWallets = MutableStateFlow(nwcWalletsLoaded.await().first),
defaultNwcWalletId = MutableStateFlow(nwcWalletsLoaded.await().second), defaultNwcWalletId = MutableStateFlow(nwcWalletsLoaded.await().second),
hideDeleteRequestDialog = hideDeleteRequestDialog, hideDeleteRequestDialog = hideDeleteRequestDialog,
@@ -683,6 +695,10 @@ object LocalPreferences {
val liveStreams: TopFilter, val liveStreams: TopFilter,
val longs: TopFilter, val longs: TopFilter,
val articles: TopFilter, val articles: TopFilter,
val badges: TopFilter,
val browseEmojiSets: TopFilter,
val communities: TopFilter,
val followPacks: TopFilter,
) )
private fun SharedPreferences.loadFollowListPrefs(): FollowListPrefs = private fun SharedPreferences.loadFollowListPrefs(): FollowListPrefs =
@@ -699,6 +715,10 @@ object LocalPreferences {
liveStreams = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, null), TopFilter.Global), liveStreams = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, null), TopFilter.Global),
longs = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null), TopFilter.Global), longs = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null), TopFilter.Global),
articles = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, null), TopFilter.AllFollows), articles = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, null), TopFilter.AllFollows),
badges = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_BADGES_FOLLOW_LIST, null), TopFilter.Mine),
browseEmojiSets = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_BROWSE_EMOJI_SETS_FOLLOW_LIST, null), TopFilter.Global),
communities = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_COMMUNITIES_FOLLOW_LIST, null), TopFilter.AllFollows),
followPacks = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_FOLLOW_PACKS_FOLLOW_LIST, null), TopFilter.Global),
) )
private inline fun <reified T : Any> parseOrNull(value: String?): T? { private inline fun <reified T : Any> parseOrNull(value: String?): T? {
@@ -497,6 +497,9 @@ class Account(
val liveCommunitiesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultCommunitiesFollowList) val liveCommunitiesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultCommunitiesFollowList)
val liveCommunitiesFollowListsPerRelay = OutboxLoaderState(liveCommunitiesFollowLists, cache, scope).flow val liveCommunitiesFollowListsPerRelay = OutboxLoaderState(liveCommunitiesFollowLists, cache, scope).flow
val liveFollowPacksFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultFollowPacksFollowList)
val liveFollowPacksFollowListsPerRelay = OutboxLoaderState(liveFollowPacksFollowLists, cache, scope).flow
override fun isWriteable(): Boolean = settings.isWriteable() override fun isWriteable(): Boolean = settings.isWriteable()
suspend fun updateWarnReports(warnReports: Boolean): Boolean { suspend fun updateWarnReports(warnReports: Boolean): Boolean {
@@ -192,6 +192,7 @@ class AccountSettings(
val defaultBadgesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Mine), val defaultBadgesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Mine),
val defaultBrowseEmojiSetsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global), val defaultBrowseEmojiSetsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultCommunitiesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows), val defaultCommunitiesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows),
val defaultFollowPacksFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val nwcWallets: MutableStateFlow<List<NwcWalletEntryNorm>> = MutableStateFlow(emptyList()), val nwcWallets: MutableStateFlow<List<NwcWalletEntryNorm>> = MutableStateFlow(emptyList()),
val defaultNwcWalletId: MutableStateFlow<String?> = MutableStateFlow(null), val defaultNwcWalletId: MutableStateFlow<String?> = MutableStateFlow(null),
var hideDeleteRequestDialog: Boolean = false, var hideDeleteRequestDialog: Boolean = false,
@@ -577,6 +578,17 @@ class AccountSettings(
} }
} }
fun changeDefaultFollowPacksFollowList(name: FeedDefinition) {
changeDefaultFollowPacksFollowList(name.code)
}
fun changeDefaultFollowPacksFollowList(name: TopFilter) {
if (defaultFollowPacksFollowList.value != name) {
defaultFollowPacksFollowList.tryEmit(name)
saveAccountSettings()
}
}
// --- // ---
// language services // language services
// --- // ---
@@ -59,7 +59,7 @@ open class DiscoverFollowSetsFeedFilter(
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems) override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
fun buildFilterParams(account: Account): FilterByListParams = open fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create( FilterByListParams.create(
account.liveDiscoveryFollowLists.value, account.liveDiscoveryFollowLists.value,
account.hiddenUsers.flow.value, account.hiddenUsers.flow.value,
@@ -102,7 +102,7 @@ fun WatchAccountForFollowPacksScreen(
feedState: FeedContentState, feedState: FeedContentState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle() val listState by accountViewModel.account.liveFollowPacksFollowLists.collectAsStateWithLifecycle()
val hiddenUsers = val hiddenUsers =
accountViewModel.account.hiddenUsers.flow accountViewModel.account.hiddenUsers.flow
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
@@ -39,14 +39,14 @@ fun FollowPacksTopBar(
nav: INav, nav: INav,
) { ) {
UserDrawerSearchTopBar(accountViewModel, nav) { UserDrawerSearchTopBar(accountViewModel, nav) {
val list by accountViewModel.account.settings.defaultDiscoveryFollowList val list by accountViewModel.account.settings.defaultFollowPacksFollowList
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
FollowPacksTopNavFilterBar( FollowPacksTopNavFilterBar(
followListsModel = accountViewModel.feedStates.feedListOptions, followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list, listName = list,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onChange = accountViewModel.account.settings::changeDefaultDiscoveryFollowList, onChange = accountViewModel.account.settings::changeDefaultFollowPacksFollowList,
) )
} }
} }
@@ -21,10 +21,20 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.DiscoverFollowSetsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.DiscoverFollowSetsFeedFilter
class FollowPacksFeedFilter( class FollowPacksFeedFilter(
account: Account, account: Account,
) : DiscoverFollowSetsFeedFilter(account) { ) : DiscoverFollowSetsFeedFilter(account) {
override fun feedKey(): String = "follow-packs-" + account.userProfile().pubkeyHex + "-" + followList().code override fun feedKey(): String = "follow-packs-" + account.userProfile().pubkeyHex + "-" + followList().code
override fun followList(): TopFilter = account.settings.defaultFollowPacksFollowList.value
override fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
account.liveFollowPacksFollowLists.value,
account.hiddenUsers.flow.value,
)
} }
@@ -52,11 +52,11 @@ class FollowPacksSubAssembler(
override fun list(key: FollowPacksQueryState) = key.listName() override fun list(key: FollowPacksQueryState) = key.listName()
fun FollowPacksQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList fun FollowPacksQueryState.listNameFlow() = account.settings.defaultFollowPacksFollowList
fun FollowPacksQueryState.listName() = listNameFlow().value fun FollowPacksQueryState.listName() = listNameFlow().value
fun FollowPacksQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay fun FollowPacksQueryState.followsPerRelayFlow() = account.liveFollowPacksFollowListsPerRelay
fun FollowPacksQueryState.followsPerRelay() = followsPerRelayFlow().value fun FollowPacksQueryState.followsPerRelay() = followsPerRelayFlow().value
@@ -58,7 +58,7 @@ private fun LongsTopNavFilterBar(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onChange: (FeedDefinition) -> Unit, onChange: (FeedDefinition) -> Unit,
) { ) {
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle() val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
FeedFilterSpinner( FeedFilterSpinner(
placeholderCode = listName, placeholderCode = listName,