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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -288,15 +290,27 @@ fun ProfileScreen(
accountViewModel: AccountViewModel,
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(repliesViewModel)
WatchLifecycleAndUpdateModel(mutualViewModel)
WatchLifecycleAndUpdateModel(appRecommendations)
if (showAppRecommendations) {
WatchLifecycleAndUpdateModel(appRecommendations)
}
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
WatchLifecycleAndUpdateModel(pinnedNotesFeedViewModel)
WatchLifecycleAndUpdateModel(galleryFeedViewModel)
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
UserProfileFilterAssemblerSubscription(
user = baseUser,
loadFollowers = showFollowersFeed,
loadZapsReceived = showZapReceivedFeed,
assembler = accountViewModel.dataSources().profile,
)
val scrollState = rememberScrollState()
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 {
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
SecondaryScrollableTabRow(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
selectedTabIndex = pagerState.currentPage,
edgePadding = Size8dp,
modifier = tabRowModifier,
divider = { HorizontalDivider(thickness = DividerThickness) },
) {
CreateAndRenderTabs(
baseUser,
pagerState,
visibleTabs,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
// key() rebuilds the pager state whenever the visible-tabs list changes,
// re-initializing it on the same tab the user was looking at.
key(visibleTabs) {
val pagerState =
rememberPagerState(
initialPage = visibleTabs.indexOf(viewedTab).coerceAtLeast(0),
pageCount = { visibleTabs.size },
)
LaunchedEffect(pagerState, visibleTabs) {
snapshotFlow { pagerState.currentPage }
.collect { page ->
visibleTabs.getOrNull(page)?.let { viewedTab = it }
}
}
HorizontalPager(
state = pagerState,
modifier = pagerModifier,
) { page ->
CreateAndRenderPages(
visibleTabs[page],
baseUser,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
pinnedNotesFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
nav,
)
Column {
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
SecondaryScrollableTabRow(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
selectedTabIndex = pagerState.currentPage,
edgePadding = Size8dp,
modifier = tabRowModifier,
divider = { HorizontalDivider(thickness = DividerThickness) },
) {
CreateAndRenderTabs(
baseUser,
pagerState,
visibleTabs,
threadsViewModel,
repliesViewModel,
mutualViewModel,
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
class UserProfileQueryState(
val user: User,
val loadFollowers: Boolean,
val loadZapsReceived: Boolean,
)
class UserProfileFilterAssembler(
@@ -28,13 +28,15 @@ import com.vitorpamplona.amethyst.model.User
@Composable
fun UserProfileFilterAssemblerSubscription(
user: User,
loadFollowers: Boolean,
loadZapsReceived: Boolean,
assembler: UserProfileFilterAssembler,
) {
// different screens get different states
// even if they are tracking the same tag.
val state =
remember(user) {
UserProfileQueryState(user)
remember(user, loadFollowers, loadZapsReceived) {
UserProfileQueryState(user, loadFollowers, loadZapsReceived)
}
LifecycleAwareKeyDataSourceSubscription(state, assembler)
@@ -32,7 +32,12 @@ class UserProfileFollowersFilterSubAssembler(
override fun updateFilter(
key: UserProfileQueryState,
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
}
@@ -33,9 +33,13 @@ class UserProfileZapsFilterSubAssembler(
key: UserProfileQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> =
listOfNotNull(
filterUserProfileZapsReceived(user(key), since),
).flatten()
if (key.loadZapsReceived) {
listOfNotNull(
filterUserProfileZapsReceived(user(key), since),
).flatten()
} else {
emptyList()
}
override fun user(key: UserProfileQueryState) = key.user
}