diff --git a/src/main/kotlin/ui/ChannelScreen.kt b/src/main/kotlin/ui/ChannelScreen.kt index b146078..e99a0e7 100644 --- a/src/main/kotlin/ui/ChannelScreen.kt +++ b/src/main/kotlin/ui/ChannelScreen.kt @@ -818,7 +818,7 @@ fun MessageScrollback( .first { item -> item == null || (item.index < capturedList.size && item.key == capturedList[item.index].seq) } - listState.scrollToItem(0) + if (stickToBottom) listState.scrollToItem(0) } } @@ -923,6 +923,31 @@ fun MessageScrollback( .height(snappedHeight) .onGloballyPositioned { containerCoords[0] = it } .pointerHoverIcon(PointerIcon.Text) + // Wheel-up means the user is reading backscroll: stop sticking to the bottom RIGHT NOW, + // on the raw wheel event. The scroll-state effect up top can miss a wheel scroll that starts + // and finishes inside one frame (isScrollInProgress is never seen as true), which left + // stickToBottom set and let the next incoming line yank the buffer back down. + // Wheel-down re-checks the resting position shortly after and re-enables sticking if the + // user is back at the bottom. + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + if (event.type == PointerEventType.Scroll) { + val dy = event.changes.firstOrNull()?.scrollDelta?.y ?: 0f + if (dy < 0f) { + stickToBottom = false + } else if (dy > 0f) { + coroutineScope.launch { + delay(200) + stickToBottom = listState.firstVisibleItemIndex == 0 && + listState.firstVisibleItemScrollOffset <= stickToBottomThresholdPx + } + } + } + } + } + } // Remembers the row under a right-click for the menu's "Copy line". Watches the // Initial pass, because ContextMenuArea takes the right-press there, before // the handler below would see it.