From fd684fff2ee8a5564e9097265c12a20c1f6c2bf6 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Tue, 23 Jun 2026 01:44:47 +0200 Subject: [PATCH] fix: scroll button direction follows scroll position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../java/dev/kaizen/app/chat/ChatScreen.kt | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index dce6002..9e9d2d5 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -908,15 +908,18 @@ fun ChatScreen( } 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 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( shape = KaizenShapes.circle, tintAlpha = GlassTiers.pill(isSystemInDarkTheme()), @@ -924,28 +927,22 @@ fun ChatScreen( cornerRadius = 18.dp, modifier = Modifier .align(Alignment.CenterEnd) - .padding(end = 8.dp, bottom = 24.dp) + .padding(end = 8.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) { - Icon(KaizenIcons.ArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp)) - } - } - } else if (!atBottom && canScrollDown) { - GlassSurface( - 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)) + Icon( + if (pointsUp) KaizenIcons.ArrowUp else KaizenIcons.ArrowDown, + null, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(20.dp), + ) } } }