Implement scroll to top in friends and conversations screens

This commit is contained in:
2025-03-29 22:31:48 +03:00
parent f1892670da
commit 5b5e8f8446
7 changed files with 71 additions and 8 deletions
@@ -36,7 +36,9 @@ import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.conversations.navigation.conversationsScreen import dev.meloda.fast.conversations.navigation.conversationsScreen
import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.friends.navigation.friendsScreen import dev.meloda.fast.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
@@ -44,6 +46,7 @@ import dev.meloda.fast.navigation.MainGraph
import dev.meloda.fast.profile.navigation.profileScreen import dev.meloda.fast.profile.navigation.profileScreen
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalScrollToTop
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
@@ -72,6 +75,14 @@ fun MainScreen(
derivedStateOf { user?.photo100 } derivedStateOf { user?.photo100 }
} }
var scrollToTop by remember {
mutableStateOf(
navigationItems.associate {
it.route to false
}
)
}
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar( NavigationBar(
@@ -101,6 +112,10 @@ fun MainScreen(
inclusive = true inclusive = true
} }
} }
} else {
scrollToTop = scrollToTop.toMutableMap().also {
it[navigationItems[index].route] = true
}
} }
}, },
icon = { icon = {
@@ -148,7 +163,8 @@ fun MainScreen(
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalHazeState provides hazeState, LocalHazeState provides hazeState,
LocalBottomPadding provides padding.calculateBottomPadding() LocalBottomPadding provides padding.calculateBottomPadding(),
LocalScrollToTop provides scrollToTop
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@@ -164,13 +180,23 @@ fun MainScreen(
friendsScreen( friendsScreen(
onError = onError, onError = onError,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked onMessageClicked = onMessageClicked,
onScrolledToTop = {
scrollToTop = scrollToTop.toMutableMap().also {
it[Friends] = false
}
},
) )
conversationsScreen( conversationsScreen(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onConversationItemClicked = onConversationItemClicked,
onCreateChatClicked = onCreateChatClicked, onCreateChatClicked = onCreateChatClicked,
navController = navController, navController = navController,
onScrolledToTop = {
scrollToTop = scrollToTop.toMutableMap().also {
it[Conversations] = false
}
}
) )
profileScreen( profileScreen(
onError = onError, onError = onError,
@@ -129,6 +129,7 @@ val LocalSizeConfig = compositionLocalOf {
val LocalHazeState = compositionLocalOf { HazeState() } val LocalHazeState = compositionLocalOf { HazeState() }
val LocalBottomPadding = compositionLocalOf { 0.dp } val LocalBottomPadding = compositionLocalOf { 0.dp }
val LocalUser = compositionLocalOf<VkUser?> { null } val LocalUser = compositionLocalOf<VkUser?> { null }
val LocalScrollToTop = compositionLocalOf { mapOf<Any, Boolean>() }
@Composable @Composable
fun AppTheme( fun AppTheme(
@@ -17,6 +17,7 @@ fun NavGraphBuilder.conversationsScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onConversationItemClicked: (id: Int) -> Unit, onConversationItemClicked: (id: Int) -> Unit,
onCreateChatClicked: () -> Unit, onCreateChatClicked: () -> Unit,
onScrolledToTop: () -> Unit,
navController: NavController, navController: NavController,
) { ) {
composable<Conversations> { composable<Conversations> {
@@ -27,6 +28,7 @@ fun NavGraphBuilder.conversationsScreen(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onConversationItemClicked = onConversationItemClicked,
onCreateChatButtonClicked = onCreateChatClicked, onCreateChatButtonClicked = onCreateChatClicked,
onScrolledToTop = onScrolledToTop,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -63,6 +63,7 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.conversations.ConversationsViewModel
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.conversations.model.ConversationsScreenState
import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.FullScreenLoader
@@ -72,6 +73,7 @@ import dev.meloda.fast.ui.model.api.ConversationOption
import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.model.api.UiConversation
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalScrollToTop
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.isScrollingUp import dev.meloda.fast.ui.util.isScrollingUp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -83,6 +85,7 @@ fun ConversationsRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onConversationItemClicked: (conversationId: Int) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit,
onCreateChatButtonClicked: () -> Unit, onCreateChatButtonClicked: () -> Unit,
onScrolledToTop: () -> Unit,
viewModel: ConversationsViewModel viewModel: ConversationsViewModel
) { ) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
@@ -105,7 +108,8 @@ fun ConversationsRoute(
onRefresh = viewModel::onRefresh, onRefresh = viewModel::onRefresh,
onCreateChatButtonClicked = onCreateChatButtonClicked, onCreateChatButtonClicked = onCreateChatButtonClicked,
setScrollIndex = viewModel::setScrollIndex, setScrollIndex = viewModel::setScrollIndex,
setScrollOffset = viewModel::setScrollOffset setScrollOffset = viewModel::setScrollOffset,
onScrolledToTop = onScrolledToTop
) )
HandleDialogs( HandleDialogs(
@@ -132,7 +136,8 @@ fun ConversationsScreen(
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onCreateChatButtonClicked: () -> Unit = {}, onCreateChatButtonClicked: () -> Unit = {},
setScrollIndex: (Int) -> Unit = {}, setScrollIndex: (Int) -> Unit = {},
setScrollOffset: (Int) -> Unit = {} setScrollOffset: (Int) -> Unit = {},
onScrolledToTop: () -> Unit = {}
) { ) {
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -145,6 +150,17 @@ fun ConversationsScreen(
initialFirstVisibleItemScrollOffset = screenState.scrollOffset initialFirstVisibleItemScrollOffset = screenState.scrollOffset
) )
val scrollToTop = LocalScrollToTop.current[Conversations] ?: false
LaunchedEffect(scrollToTop) {
if (scrollToTop) {
if (listState.firstVisibleItemIndex > 14) {
listState.scrollToItem(14)
}
listState.animateScrollToItem(0)
onScrolledToTop()
}
}
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.debounce(500L) .debounce(500L)
@@ -12,13 +12,15 @@ object Friends
fun NavGraphBuilder.friendsScreen( fun NavGraphBuilder.friendsScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit onMessageClicked: (userId: Int) -> Unit,
onScrolledToTop: () -> Unit
) { ) {
composable<Friends> { composable<Friends> {
FriendsRoute( FriendsRoute(
onError = onError, onError = onError,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked onMessageClicked = onMessageClicked,
onScrolledToTop = onScrolledToTop
) )
} }
} }
@@ -29,12 +29,14 @@ import dev.chrisbanes.haze.hazeSource
import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalScrollToTop
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -51,7 +53,8 @@ fun FriendsScreen(
onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userId: Int) -> Unit = {}, onMessageClicked: (userId: Int) -> Unit = {},
setCanScrollBackward: (Boolean) -> Unit = {} setCanScrollBackward: (Boolean) -> Unit = {},
onScrolledToTop: () -> Unit = {}
) { ) {
val context: Context = LocalContext.current val context: Context = LocalContext.current
val viewModel: FriendsViewModel = val viewModel: FriendsViewModel =
@@ -93,6 +96,17 @@ fun FriendsScreen(
initialFirstVisibleItemScrollOffset = screenState.scrollOffset initialFirstVisibleItemScrollOffset = screenState.scrollOffset
) )
val scrollToTop = LocalScrollToTop.current[Friends] ?: false
LaunchedEffect(scrollToTop) {
if (scrollToTop) {
if (listState.firstVisibleItemIndex > 14) {
listState.scrollToItem(14)
}
listState.animateScrollToItem(0)
onScrolledToTop()
}
}
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.debounce(250L) .debounce(250L)
@@ -58,6 +58,7 @@ fun FriendsRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit, onMessageClicked: (userId: Int) -> Unit,
onScrolledToTop: () -> Unit
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -236,7 +237,8 @@ fun FriendsRoute(
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
setCanScrollBackward = { canScrollBackward = it } setCanScrollBackward = { canScrollBackward = it },
onScrolledToTop = onScrolledToTop
) )
} }
} }