From 4544d88f8dfbf4a7592715cb9c2d0c0f18bf03a7 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:12:25 +0300 Subject: [PATCH] feat(#130): add scroll-to-bottom FAB that appears when user scrolls up (#163) Co-authored-by: M57 --- .../m57/hermescontrol/ui/chat/ChatScreen.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index becb2f82..88018148 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.BorderStroke @@ -59,6 +61,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -73,6 +76,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -109,6 +113,11 @@ fun ChatScreen( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val listState = rememberLazyListState() + val showScrollToBottom by remember { + derivedStateOf { + state.messages.isNotEmpty() && listState.canScrollForward + } + } var inputText by rememberSaveable { mutableStateOf("") } val snackbarHostState = remember { SnackbarHostState() } val isDark = isSystemInDarkTheme() @@ -522,6 +531,31 @@ fun ChatScreen( } } } + + // Scroll-to-bottom FAB + AnimatedVisibility( + visible = showScrollToBottom, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(end = 16.dp, bottom = 16.dp), + ) { + FloatingActionButton( + onClick = { + listState.animateScrollToItem(state.messages.lastIndex) + }, + modifier = Modifier.size(40.dp), + containerColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = "Scroll to bottom", + ) + } + } } ChatInputBar(