From 251699beaa8bb7d981664d5c6708be531188e452 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 19:33:16 +0000 Subject: [PATCH] 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. --- .../screen/loggedIn/profile/ProfileScreen.kt | 128 +++++++++++------- .../datasource/UserProfileFilterAssembler.kt | 2 + .../UserProfileFilterAssemblerSubscription.kt | 6 +- .../UserProfileFollowersFilterSubAssembler.kt | 7 +- .../UserProfileZapsFilterSubAssembler.kt | 10 +- 5 files changed, 100 insertions(+), 53 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 3d19ac70d..72b6702c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -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, + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index b57daf7ce..e601c434f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt index 9b86949c8..fe2d6b28f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt index 6fe462fe8..91e16ad15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt @@ -32,7 +32,12 @@ class UserProfileFollowersFilterSubAssembler( override fun updateFilter( key: UserProfileQueryState, since: SincePerRelayMap?, - ): List = filterUserProfileFollowers(user(key), since) + ): List = + if (key.loadFollowers) { + filterUserProfileFollowers(user(key), since) + } else { + emptyList() + } override fun user(key: UserProfileQueryState) = key.user } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt index 5546024d0..80681cdb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt @@ -33,9 +33,13 @@ class UserProfileZapsFilterSubAssembler( key: UserProfileQueryState, since: SincePerRelayMap?, ): List = - listOfNotNull( - filterUserProfileZapsReceived(user(key), since), - ).flatten() + if (key.loadZapsReceived) { + listOfNotNull( + filterUserProfileZapsReceived(user(key), since), + ).flatten() + } else { + emptyList() + } override fun user(key: UserProfileQueryState) = key.user }