fix: honor Profile UI toggles in subscriptions and pager state

- Gate WatchLifecycleAndUpdateModel(appRecommendations) on the
  showProfileAppRecommendations toggle so the viewmodel no longer
  refreshes when the section is hidden.
- Thread loadFollowers / loadZapsReceived through UserProfileQueryState
  and the matching sub-assemblers. When the user hides those tabs,
  UserProfileFilterAssembler stops emitting their relay filters and
  the live REQs drop.
- Re-pin the profile pager to the tab the user was viewing whenever
  the visible-tabs list changes, using key(visibleTabs) +
  rememberPagerState(initialPage = ...) and a snapshotFlow that tracks
  the currently-viewed ProfileTab. Prevents the pager from silently
  shifting to a different tab when one is toggled off in settings.
This commit is contained in:
Claude
2026-05-13 19:33:16 +00:00
parent f26f3626f1
commit 251699beaa
5 changed files with 100 additions and 53 deletions
@@ -46,10 +46,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -288,15 +290,27 @@ fun ProfileScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val ui = accountViewModel.settings.uiSettingsFlow
val showAppRecommendations by ui.showProfileAppRecommendations.collectAsStateWithLifecycle()
val showFollowersFeed by ui.showProfileFollowersFeed.collectAsStateWithLifecycle()
val showZapReceivedFeed by ui.showProfileZapReceivedFeed.collectAsStateWithLifecycle()
WatchLifecycleAndUpdateModel(threadsViewModel) WatchLifecycleAndUpdateModel(threadsViewModel)
WatchLifecycleAndUpdateModel(repliesViewModel) WatchLifecycleAndUpdateModel(repliesViewModel)
WatchLifecycleAndUpdateModel(mutualViewModel) WatchLifecycleAndUpdateModel(mutualViewModel)
WatchLifecycleAndUpdateModel(appRecommendations) if (showAppRecommendations) {
WatchLifecycleAndUpdateModel(appRecommendations)
}
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel) WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
WatchLifecycleAndUpdateModel(pinnedNotesFeedViewModel) WatchLifecycleAndUpdateModel(pinnedNotesFeedViewModel)
WatchLifecycleAndUpdateModel(galleryFeedViewModel) WatchLifecycleAndUpdateModel(galleryFeedViewModel)
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile) UserProfileFilterAssemblerSubscription(
user = baseUser,
loadFollowers = showFollowersFeed,
loadZapsReceived = showZapReceivedFeed,
assembler = accountViewModel.dataSources().profile,
)
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -461,54 +475,74 @@ private fun RenderScreen(
} }
} }
val pagerState = rememberPagerState(pageCount = { visibleTabs.size }) // Track the tab the user was viewing so we can re-pin to it when the
// visible-tabs list changes (e.g. they hid Followers from settings while
// standing on the Zaps tab).
var viewedTab by remember { mutableStateOf(visibleTabs.first()) }
Column { // key() rebuilds the pager state whenever the visible-tabs list changes,
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel) // re-initializing it on the same tab the user was looking at.
SecondaryScrollableTabRow( key(visibleTabs) {
containerColor = Color.Transparent, val pagerState =
contentColor = MaterialTheme.colorScheme.onBackground, rememberPagerState(
selectedTabIndex = pagerState.currentPage, initialPage = visibleTabs.indexOf(viewedTab).coerceAtLeast(0),
edgePadding = Size8dp, pageCount = { visibleTabs.size },
modifier = tabRowModifier,
divider = { HorizontalDivider(thickness = DividerThickness) },
) {
CreateAndRenderTabs(
baseUser,
pagerState,
visibleTabs,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
) )
LaunchedEffect(pagerState, visibleTabs) {
snapshotFlow { pagerState.currentPage }
.collect { page ->
visibleTabs.getOrNull(page)?.let { viewedTab = it }
}
} }
HorizontalPager(
state = pagerState, Column {
modifier = pagerModifier, ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
) { page -> SecondaryScrollableTabRow(
CreateAndRenderPages( containerColor = Color.Transparent,
visibleTabs[page], contentColor = MaterialTheme.colorScheme.onBackground,
baseUser, selectedTabIndex = pagerState.currentPage,
threadsViewModel, edgePadding = Size8dp,
repliesViewModel, modifier = tabRowModifier,
mutualViewModel, divider = { HorizontalDivider(thickness = DividerThickness) },
followsFeedViewModel, ) {
followersFeedViewModel, CreateAndRenderTabs(
zapFeedViewModel, baseUser,
bookmarksFeedViewModel, pagerState,
pinnedNotesFeedViewModel, visibleTabs,
galleryFeedViewModel, threadsViewModel,
reportsFeedViewModel, repliesViewModel,
accountViewModel, mutualViewModel,
nav, followsFeedViewModel,
) followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
)
}
HorizontalPager(
state = pagerState,
modifier = pagerModifier,
) { page ->
CreateAndRenderPages(
visibleTabs[page],
baseUser,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
pinnedNotesFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
nav,
)
}
} }
} }
} }
@@ -27,6 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class UserProfileQueryState( class UserProfileQueryState(
val user: User, val user: User,
val loadFollowers: Boolean,
val loadZapsReceived: Boolean,
) )
class UserProfileFilterAssembler( class UserProfileFilterAssembler(
@@ -28,13 +28,15 @@ import com.vitorpamplona.amethyst.model.User
@Composable @Composable
fun UserProfileFilterAssemblerSubscription( fun UserProfileFilterAssemblerSubscription(
user: User, user: User,
loadFollowers: Boolean,
loadZapsReceived: Boolean,
assembler: UserProfileFilterAssembler, assembler: UserProfileFilterAssembler,
) { ) {
// different screens get different states // different screens get different states
// even if they are tracking the same tag. // even if they are tracking the same tag.
val state = val state =
remember(user) { remember(user, loadFollowers, loadZapsReceived) {
UserProfileQueryState(user) UserProfileQueryState(user, loadFollowers, loadZapsReceived)
} }
LifecycleAwareKeyDataSourceSubscription(state, assembler) LifecycleAwareKeyDataSourceSubscription(state, assembler)
@@ -32,7 +32,12 @@ class UserProfileFollowersFilterSubAssembler(
override fun updateFilter( override fun updateFilter(
key: UserProfileQueryState, key: UserProfileQueryState,
since: SincePerRelayMap?, since: SincePerRelayMap?,
): List<RelayBasedFilter> = filterUserProfileFollowers(user(key), since) ): List<RelayBasedFilter> =
if (key.loadFollowers) {
filterUserProfileFollowers(user(key), since)
} else {
emptyList()
}
override fun user(key: UserProfileQueryState) = key.user override fun user(key: UserProfileQueryState) = key.user
} }
@@ -33,9 +33,13 @@ class UserProfileZapsFilterSubAssembler(
key: UserProfileQueryState, key: UserProfileQueryState,
since: SincePerRelayMap?, since: SincePerRelayMap?,
): List<RelayBasedFilter> = ): List<RelayBasedFilter> =
listOfNotNull( if (key.loadZapsReceived) {
filterUserProfileZapsReceived(user(key), since), listOfNotNull(
).flatten() filterUserProfileZapsReceived(user(key), since),
).flatten()
} else {
emptyList()
}
override fun user(key: UserProfileQueryState) = key.user override fun user(key: UserProfileQueryState) = key.user
} }