Compare commits

...

3 Commits

Author SHA1 Message Date
Claude
8103a8da47 refactor(apps): bundle TopNavFilterSource + fix local-pipeline leak
Two issues from the first Apps pass:

1) Lifetime leak. The local OutboxLoaderState + FeedTopNavFilterState
   ran on account.scope (via account.topNavFilterFlow), so the per-route
   pipeline kept eagerly resolving filters after the screen left the
   back stack and until logout. account.topNavFilterFlow now accepts an
   optional scope (default: account.scope, preserving call sites), and
   the local builder threads a rememberCoroutineScope() through both the
   FeedTopNavFilterState and the OutboxLoaderState — and through the
   local FeedContentState — so the pipeline cancels on disposal.

2) Parameter explosion. SoftwareAppsScreen previously took four
   separate flows + setter. They are now bundled into a TopNavFilterSource
   value (topFilterFlow, liveFollowListsFlow, followsPerRelayFlow,
   onChange), and rememberLocalTopNavFilterSource produces a fresh one
   for an override entry. Screen, TopBar, and the subscription helper
   each pick the fields they need. The Shared/Filtered dispatch in
   SoftwareAppsScreen collapses into one branch that picks feed +
   source per route.

https://claude.ai/code/session_01MEX5TzpuHwnHG5a1hqtHKZ
2026-05-28 02:05:37 +00:00
Claude
d103fdd2d1 refactor(apps): drop implicit account defaults from SoftwareAppsFeedFilter
Filter ctor now takes topFilterFlow and liveFollowListsFlow as required
params. The shared (account-default) wiring moves into the single call
site in AccountFeedContentStates, matching the existing override path
in SoftwareAppsScreen. Both call sites now look identical from the
filter's perspective.

https://claude.ai/code/session_01MEX5TzpuHwnHG5a1hqtHKZ
2026-05-27 23:29:09 +00:00
Claude
3d68b0894c feat(apps): route Apps screen by TopFilter (clickable topic chips)
Make Route.SoftwareApps carry an optional TopFilter so the screen can
be pushed in a pre-filtered state without mutating the persisted
default. Clicking a topic chip on a Software Application card now
opens the Apps screen again, filtered by that hashtag, as a separate
back-stack entry. Filter changes via the spinner on the filtered
screen are non-persistent — they affect only that stack entry.

The shared (unfiltered) entry continues to use the singleton feed
pipeline on AccountFeedContentStates; the filtered entry builds a
local pipeline (TopFilter flow → topNavFilterFlow → OutboxLoaderState
→ FeedContentState) scoped to the back-stack entry, and registers an
independent SoftwareAppsQueryState with the existing
SoftwareAppsFilterAssembler so its relay subscriptions are managed
the same way.

https://claude.ai/code/session_01MEX5TzpuHwnHG5a1hqtHKZ
2026-05-27 22:58:45 +00:00
13 changed files with 243 additions and 60 deletions

View File

@@ -453,23 +453,25 @@ class Account(
geohashCache = geohashListDecryptionCache,
)
fun topNavFilterFlow(listName: MutableStateFlow<TopFilter>) =
FeedTopNavFilterState(
feedFilterListName = listName,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
relayFeeds = relayFeedsList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
favoriteAlgoFeedsOrchestrator = favoriteAlgoFeedsOrchestrator,
favoriteAlgoFeedAddresses = favoriteAlgoFeedsList.flow,
interestSetHashtags = interestSets.hashtagsByIdentifier,
).flow
fun topNavFilterFlow(
listName: MutableStateFlow<TopFilter>,
scope: CoroutineScope = this.scope,
) = FeedTopNavFilterState(
feedFilterListName = listName,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
relayFeeds = relayFeedsList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
favoriteAlgoFeedsOrchestrator = favoriteAlgoFeedsOrchestrator,
favoriteAlgoFeedAddresses = favoriteAlgoFeedsList.flow,
interestSetHashtags = interestSets.hashtagsByIdentifier,
).flow
// App-ready Feeds
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultHomeFollowList)

View File

@@ -264,7 +264,7 @@ fun BuildNavigation(
composableFromEnd<Route.ProfileBadges> { ProfileBadgesScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) }
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
composableFromEnd<Route.SoftwareApps> { SoftwareAppsScreen(accountViewModel, nav) }
composableFromEndArgs<Route.SoftwareApps> { SoftwareAppsScreen(it, accountViewModel, nav) }
composableFromEnd<Route.Calendars> { CalendarsScreen(accountViewModel, nav) }
composableFromEnd<Route.CalendarCollections> { CalendarCollectionsScreen(accountViewModel, nav) }
composableFromEnd<Route.CalendarReminderSettings> { CalendarReminderSettingsScreen(nav) }

View File

@@ -209,7 +209,7 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
id = NavBarItem.SOFTWARE_APPS,
labelRes = R.string.software_apps,
icon = MaterialSymbols.Apps,
resolveRoute = { Route.SoftwareApps },
resolveRoute = { Route.SoftwareApps() },
),
NavBarItem.CALENDARS to
NavBarItemDef(

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.navigation.routes
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.toRoute
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -81,7 +82,9 @@ sealed class Route {
@Serializable object Pictures : Route()
@Serializable object SoftwareApps : Route()
@Serializable data class SoftwareApps(
val topFilter: TopFilter? = null,
) : Route()
@Serializable object Calendars : Route()

View File

@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -56,9 +57,11 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.LinkIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -170,7 +173,14 @@ fun RenderSoftwareApplication(
if (platforms.isNotEmpty() || topics.isNotEmpty() || license != null) {
Spacer(StdVertSpacer)
ChipFlowRow(platforms = platforms, topics = topics, license = license)
ChipFlowRow(
platforms = platforms,
topics = topics,
license = license,
onTopicClick = { topic ->
nav.nav(Route.SoftwareApps(TopFilter.Hashtag(topic.lowercase())))
},
)
}
if (website != null || repo != null) {
@@ -206,11 +216,12 @@ private fun ChipFlowRow(
platforms: List<String>,
topics: List<String>,
license: String?,
onTopicClick: (String) -> Unit,
) {
// FlowRow is in compose foundation but we use a simple LazyRow to keep this compatible.
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
items(platforms) { Chip(it) }
items(topics) { Chip("#$it") }
items(topics) { topic -> Chip("#$topic", onClick = { onTopicClick(topic) }) }
license?.let { item { Chip(it, tint = MaterialTheme.colorScheme.secondaryContainer) } }
}
}
@@ -219,13 +230,14 @@ private fun ChipFlowRow(
private fun Chip(
text: String,
tint: Color = MaterialTheme.colorScheme.surfaceVariant,
onClick: (() -> Unit)? = null,
) {
Box(
val base =
Modifier
.clip(RoundedCornerShape(12.dp))
.background(tint)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
val withClick = if (onClick != null) base.clickable(onClick = onClick) else base
Box(withClick.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text(text = text, fontSize = 11.sp, style = MaterialTheme.typography.labelSmall)
}
}

View File

@@ -116,7 +116,16 @@ class AccountFeedContentStates(
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
val musicTracksFeed = FeedContentState(MusicTracksFeedFilter(account), scope, LocalCache)
val musicPlaylistsFeed = FeedContentState(MusicPlaylistsFeedFilter(account), scope, LocalCache)
val softwareAppsFeed = FeedContentState(SoftwareAppsFeedFilter(account), scope, LocalCache)
val softwareAppsFeed =
FeedContentState(
SoftwareAppsFeedFilter(
account = account,
topFilterFlow = account.settings.defaultSoftwareAppsFollowList,
liveFollowListsFlow = account.liveSoftwareAppsFollowLists,
),
scope,
LocalCache,
)
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
val notificationsFollowing = CardFeedContentState(NotificationFeedFilter(account, TopFilter.AllFollows), scope)

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2025 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.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Bundle of every flow a top-nav-filter consumer needs: the current
* [TopFilter], the resolved follow list, the per-relay routing for relay
* subscriptions, and a setter callback. Screens hand this to their TopBar,
* their DAL filter (via [topFilterFlow] + [liveFollowListsFlow]), and their
* relay subscription assembler (via [topFilterFlow] + [followsPerRelayFlow]).
*
* Use the account-default source (built from `account.settings.defaultXxxFollowList`
* and the matching `account.liveXxxFollowLists*` flows) for the regular tab entry,
* and [rememberLocalTopNavFilterSource] for a per-route override scoped to a
* single back-stack entry.
*/
@Stable
class TopNavFilterSource(
val topFilterFlow: StateFlow<TopFilter>,
val liveFollowListsFlow: StateFlow<IFeedTopNavFilter>,
val followsPerRelayFlow: StateFlow<IFeedTopNavPerRelayFilterSet>,
val onChange: (TopFilter) -> Unit,
)
/**
* Per-screen-instance TopNavFilterSource: builds a local [MutableStateFlow]
* seeded with [initial], then derives the resolved follow list and per-relay
* routing from it. The internal flows run on the supplied [scope] — callers
* should pass `rememberCoroutineScope()` so the pipeline cancels when the
* screen leaves composition, instead of `account.scope` which would keep
* the flows alive until logout.
*/
@Composable
fun rememberLocalTopNavFilterSource(
account: Account,
initial: TopFilter,
scope: CoroutineScope,
cache: LocalCache = LocalCache,
): TopNavFilterSource =
remember(account, initial, scope) {
val filter = MutableStateFlow(initial)
val live = account.topNavFilterFlow(filter, scope)
val perRelay = OutboxLoaderState(live, cache, scope).flow
TopNavFilterSource(
topFilterFlow = filter,
liveFollowListsFlow = live,
followsPerRelayFlow = perRelay,
onChange = { filter.tryEmit(it) },
)
}

View File

@@ -29,11 +29,14 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
@@ -45,6 +48,9 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.types.RenderSoftwareApplication
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TopNavFilterSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.rememberLocalTopNavFilterSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.dal.SoftwareAppsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -52,34 +58,85 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw
@Composable
fun SoftwareAppsScreen(
route: Route.SoftwareApps,
accountViewModel: AccountViewModel,
nav: INav,
) {
SoftwareAppsScreen(
feedContentState = accountViewModel.feedStates.softwareAppsFeed,
accountViewModel = accountViewModel,
nav = nav,
)
val account = accountViewModel.account
val override = route.topFilter
if (override == null) {
val sharedSource =
remember(account) {
TopNavFilterSource(
topFilterFlow = account.settings.defaultSoftwareAppsFollowList,
liveFollowListsFlow = account.liveSoftwareAppsFollowLists,
followsPerRelayFlow = account.liveSoftwareAppsFollowListsPerRelay,
onChange = account.settings::changeDefaultSoftwareAppsFollowList,
)
}
SoftwareAppsScreen(
feedContentState = accountViewModel.feedStates.softwareAppsFeed,
source = sharedSource,
accountViewModel = accountViewModel,
nav = nav,
)
} else {
val scope = rememberCoroutineScope()
val localSource = rememberLocalTopNavFilterSource(account, override, scope)
val localFeed =
remember(account, localSource, scope) {
FeedContentState(
SoftwareAppsFeedFilter(
account = account,
topFilterFlow = localSource.topFilterFlow,
liveFollowListsFlow = localSource.liveFollowListsFlow,
),
scope,
LocalCache,
)
}
SoftwareAppsScreen(
feedContentState = localFeed,
source = localSource,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable
fun SoftwareAppsScreen(
feedContentState: FeedContentState,
source: TopNavFilterSource,
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchLifecycleAndUpdateModel(feedContentState)
WatchAccountForSoftwareAppsScreen(feedContentState = feedContentState, accountViewModel = accountViewModel)
SoftwareAppsFilterAssemblerSubscription(accountViewModel)
WatchAccountForSoftwareAppsScreen(
feedContentState = feedContentState,
source = source,
accountViewModel = accountViewModel,
)
SoftwareAppsFilterAssemblerSubscription(
dataSource = accountViewModel.dataSources().softwareApps,
feedContentState = feedContentState,
topFilterFlow = source.topFilterFlow,
followsPerRelayFlow = source.followsPerRelayFlow,
accountViewModel = accountViewModel,
)
DisappearingScaffold(
isInvertedLayout = false,
topBar = {
SoftwareAppsTopBar(accountViewModel, nav)
SoftwareAppsTopBar(
source = source,
accountViewModel = accountViewModel,
nav = nav,
)
},
bottomBar = {
AppBottomBar(Route.SoftwareApps, nav, accountViewModel) { route ->
if (route == Route.SoftwareApps) {
AppBottomBar(Route.SoftwareApps(), nav, accountViewModel) { route ->
if (route is Route.SoftwareApps) {
feedContentState.sendToTop()
} else {
nav.navBottomBar(route)
@@ -113,9 +170,10 @@ fun SoftwareAppsScreen(
@Composable
fun WatchAccountForSoftwareAppsScreen(
feedContentState: FeedContentState,
source: TopNavFilterSource,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveSoftwareAppsFollowLists.collectAsStateWithLifecycle()
val listState by source.liveFollowListsFlow.collectAsStateWithLifecycle()
val hiddenUsers =
accountViewModel.account.hiddenUsers.flow
.collectAsStateWithLifecycle()

View File

@@ -31,22 +31,23 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TopNavFilterSource
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun SoftwareAppsTopBar(
source: TopNavFilterSource,
accountViewModel: AccountViewModel,
nav: INav,
) {
UserDrawerSearchTopBar(accountViewModel, nav) {
val list by accountViewModel.account.settings.defaultSoftwareAppsFollowList
.collectAsStateWithLifecycle()
val list by source.topFilterFlow.collectAsStateWithLifecycle()
SoftwareAppsTopNavFilterBar(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel = accountViewModel,
onChange = accountViewModel.account.settings::changeDefaultSoftwareAppsFollowList,
onChange = { source.onChange(it.code) },
)
}
}

View File

@@ -25,19 +25,23 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
import kotlinx.coroutines.flow.StateFlow
class SoftwareAppsFeedFilter(
val account: Account,
val topFilterFlow: StateFlow<TopFilter>,
val liveFollowListsFlow: StateFlow<IFeedTopNavFilter>,
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
override fun limit() = 200
fun followList(): TopFilter = account.settings.defaultSoftwareAppsFollowList.value
fun followList(): TopFilter = topFilterFlow.value
fun TopFilter.isMuteList() = this is TopFilter.MuteList
@@ -61,7 +65,7 @@ class SoftwareAppsFeedFilter(
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
account.liveSoftwareAppsFollowLists.value,
liveFollowListsFlow.value,
account.hiddenUsers.flow.value,
)

View File

@@ -22,14 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
class SoftwareAppsQueryState(
val account: Account,
val feedStates: AccountFeedContentStates,
val feedContentState: FeedContentState,
val topFilterFlow: StateFlow<TopFilter>,
val followsPerRelayFlow: StateFlow<IFeedTopNavPerRelayFilterSet>,
val scope: CoroutineScope,
)

View File

@@ -24,12 +24,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.flow.StateFlow
@Composable
fun SoftwareAppsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
SoftwareAppsFilterAssemblerSubscription(
accountViewModel.dataSources().softwareApps,
accountViewModel.feedStates.softwareAppsFeed,
accountViewModel.account.settings.defaultSoftwareAppsFollowList,
accountViewModel.account.liveSoftwareAppsFollowListsPerRelay,
accountViewModel,
)
}
@@ -37,11 +44,20 @@ fun SoftwareAppsFilterAssemblerSubscription(accountViewModel: AccountViewModel)
@Composable
fun SoftwareAppsFilterAssemblerSubscription(
dataSource: SoftwareAppsFilterAssembler,
feedContentState: FeedContentState,
topFilterFlow: StateFlow<TopFilter>,
followsPerRelayFlow: StateFlow<IFeedTopNavPerRelayFilterSet>,
accountViewModel: AccountViewModel,
) {
val state =
remember(accountViewModel.account) {
SoftwareAppsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
remember(accountViewModel.account, feedContentState, topFilterFlow, followsPerRelayFlow) {
SoftwareAppsQueryState(
account = accountViewModel.account,
feedContentState = feedContentState,
topFilterFlow = topFilterFlow,
followsPerRelayFlow = followsPerRelayFlow,
scope = accountViewModel.viewModelScope,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)

View File

@@ -42,22 +42,14 @@ class SoftwareAppsSubAssembler(
key: SoftwareAppsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
val feedSettings = key.followsPerRelayFlow.value
return makeSoftwareAppsFilter(feedSettings, since, key.feedStates.softwareAppsFeed.lastNoteCreatedAtIfFilled())
return makeSoftwareAppsFilter(feedSettings, since, key.feedContentState.lastNoteCreatedAtIfFilled())
}
override fun user(key: SoftwareAppsQueryState) = key.account.userProfile()
override fun list(key: SoftwareAppsQueryState) = key.listName()
fun SoftwareAppsQueryState.listNameFlow() = account.settings.defaultSoftwareAppsFollowList
fun SoftwareAppsQueryState.listName() = listNameFlow().value
fun SoftwareAppsQueryState.followsPerRelayFlow() = account.liveSoftwareAppsFollowListsPerRelay
fun SoftwareAppsQueryState.followsPerRelay() = followsPerRelayFlow().value
override fun list(key: SoftwareAppsQueryState) = key.topFilterFlow.value
val userJobMap = mutableMapOf<User, List<Job>>()
@@ -68,17 +60,17 @@ class SoftwareAppsSubAssembler(
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
key.topFilterFlow.collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
key.followsPerRelayFlow.sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.softwareAppsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
key.feedContentState.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},