fix: scroll button direction follows scroll position

Single button instead of two mutually exclusive ones. Arrow direction
is based on proximity to bottom: near bottom → arrow up (scroll to top),
not near bottom → arrow down (scroll to end). Updates instantly as the
user scrolls. Hidden when already at the target end.

Old logic had an if/else that always showed ArrowUp first (because
!atTop was always true when scrolled down), making ArrowDown unreachable
in most scroll positions.
This commit is contained in:
Bruno Deanoz 2026-06-23 01:44:47 +02:00
parent f855a26b31
commit fd684fff2e

View file

@ -908,15 +908,18 @@ fun ChatScreen(
} }
if (!isStreaming) { if (!isStreaming) {
val atTop by remember { derivedStateOf { listState.firstVisibleItemIndex <= 1 } }
val atBottom by remember { derivedStateOf {
val info = listState.layoutInfo
info.visibleItemsInfo.lastOrNull()?.index == info.totalItemsCount - 1
}}
val canScrollUp by remember { derivedStateOf { listState.canScrollBackward } } val canScrollUp by remember { derivedStateOf { listState.canScrollBackward } }
val canScrollDown by remember { derivedStateOf { listState.canScrollForward } } val canScrollDown by remember { derivedStateOf { listState.canScrollForward } }
val showButton = canScrollUp || canScrollDown
val nearBottom by remember { derivedStateOf {
val info = listState.layoutInfo
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0
val total = info.totalItemsCount
if (total <= 0) true else lastVisible >= total - 2
}}
val pointsUp = !nearBottom && canScrollUp
if (!atTop && canScrollUp) { if (showButton && !(canScrollUp && !canScrollDown && nearBottom)) {
GlassSurface( GlassSurface(
shape = KaizenShapes.circle, shape = KaizenShapes.circle,
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()), tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
@ -924,28 +927,22 @@ fun ChatScreen(
cornerRadius = 18.dp, cornerRadius = 18.dp,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
.padding(end = 8.dp, bottom = 24.dp) .padding(end = 8.dp)
.size(36.dp) .size(36.dp)
.clickable { scope.launch { listState.animateScrollToItem(0) } }, .clickable {
scope.launch {
if (pointsUp) listState.animateScrollToItem(0)
else listState.animateScrollToItem(messages.size - 1)
}
},
) { ) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(KaizenIcons.ArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp)) Icon(
} if (pointsUp) KaizenIcons.ArrowUp else KaizenIcons.ArrowDown,
} null,
} else if (!atBottom && canScrollDown) { tint = MaterialTheme.colorScheme.onBackground,
GlassSurface( modifier = Modifier.size(20.dp),
shape = KaizenShapes.circle, )
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
shadowStack = KaizenShadows.level1,
cornerRadius = 18.dp,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp, top = 24.dp)
.size(36.dp)
.clickable { scope.launch { listState.animateScrollToItem(messages.size - 1) } },
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(KaizenIcons.ArrowDown, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
} }
} }
} }