Chat: scroll to bottom of newest message on open and on new messages
scrollToItem(last) top-aligns the last row, so a final message taller than the viewport stayed cut off at the bottom. New scrollToBottom() brings the last row into view, then aligns its bottom edge with the viewport bottom. The isAtBottom check now also treats 'can't scroll forward' as at-bottom, so auto-scroll and the back-to-bottom button behave correctly when the newest message is taller than the viewport.
This commit is contained in:
1 parent
6591d7cec0
commit
dd43033888
1 file changed
+31
-15
@@ -167,10 +167,14 @@ fun ChatScreen(controller: IrisController) {
|
||||
val threads = channels.filter { it.kind == "thread" && it.parentChatId == currentChatId }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
// "At the bottom" while the last row's bottom edge is within a small
|
||||
// tolerance of its rest position (16dp above the viewport bottom, from the
|
||||
// list's content padding). Scrolling up by more than that reveals the
|
||||
// back-to-bottom button and stops new messages from auto-scrolling the view.
|
||||
// "At the bottom" when the list can't scroll further forward, or the last
|
||||
// row's bottom edge is within a small tolerance of the viewport bottom
|
||||
// (the tolerance avoids flicker when the newest message is shorter than
|
||||
// the viewport). The canScrollForward check is what makes this work when
|
||||
// the newest message is TALLER than the viewport: the gap is then
|
||||
// negative even at the true bottom. Scrolling up by more than the
|
||||
// tolerance reveals the back-to-bottom button and stops new messages from
|
||||
// auto-scrolling the view.
|
||||
val bottomThresholdPx = 8.dp.value * LocalDensity.current.density
|
||||
var isAtBottom by remember { mutableStateOf(true) }
|
||||
var isAutoScrolling by remember { mutableStateOf(false) }
|
||||
@@ -182,10 +186,28 @@ fun ChatScreen(controller: IrisController) {
|
||||
true
|
||||
} else {
|
||||
val gap = (info.viewportEndOffset - (last.offset + last.size)).toFloat()
|
||||
last.index == info.totalItemsCount - 1 && gap >= bottomThresholdPx
|
||||
!listState.canScrollForward ||
|
||||
(last.index == info.totalItemsCount - 1 && gap >= bottomThresholdPx)
|
||||
}
|
||||
}.collect { isAtBottom = it }
|
||||
}
|
||||
|
||||
// Scroll so the newest row's BOTTOM edge is visible: first bring the last
|
||||
// row into view (top-aligned), then, if it is taller than the viewport,
|
||||
// align its bottom with the viewport bottom. A plain scrollToItem(last)
|
||||
// would leave a long final message cut off at the bottom.
|
||||
suspend fun scrollToBottom(animate: Boolean) {
|
||||
val total = listState.layoutInfo.totalItemsCount
|
||||
if (total == 0) return
|
||||
val last = total - 1
|
||||
if (animate) listState.animateScrollToItem(last) else listState.scrollToItem(last)
|
||||
val info = listState.layoutInfo
|
||||
val item = info.visibleItemsInfo.firstOrNull { it.index == last } ?: return
|
||||
val offset = (item.offset + item.size - info.viewportSize.height).coerceAtLeast(0)
|
||||
if (offset > 0) {
|
||||
if (animate) listState.animateScrollToItem(last, offset) else listState.scrollToItem(last, offset)
|
||||
}
|
||||
}
|
||||
var input by remember { mutableStateOf("") }
|
||||
var showSearch by remember { mutableStateOf(false) }
|
||||
var showNewChannel by remember { mutableStateOf(false) }
|
||||
@@ -235,11 +257,8 @@ fun ChatScreen(controller: IrisController) {
|
||||
focusManager.clearFocus(force = true)
|
||||
}
|
||||
|
||||
// (Re)entering a lane always lands on the latest message.
|
||||
LaunchedEffect(currentLane) {
|
||||
val last = items.size - 1
|
||||
if (last >= 0) listState.scrollToItem(last)
|
||||
}
|
||||
// (Re)entering a lane always lands on the latest message (bottom-aligned).
|
||||
LaunchedEffect(currentLane) { scrollToBottom(animate = false) }
|
||||
|
||||
// Follow the bottom only while the user is already there; if they've
|
||||
// scrolled up to read history, new messages must not yank the view back
|
||||
@@ -250,7 +269,7 @@ fun ChatScreen(controller: IrisController) {
|
||||
if (last >= 0) {
|
||||
isAutoScrolling = true
|
||||
try {
|
||||
listState.animateScrollToItem(last)
|
||||
scrollToBottom(animate = true)
|
||||
} finally {
|
||||
isAutoScrolling = false
|
||||
}
|
||||
@@ -551,10 +570,7 @@ fun ChatScreen(controller: IrisController) {
|
||||
.background(IrisColors.surface)
|
||||
.border(1.dp, IrisColors.divider, CircleShape)
|
||||
.clickable {
|
||||
val last = listState.layoutInfo.totalItemsCount - 1
|
||||
if (last >= 0) {
|
||||
drawerScope.launch { listState.animateScrollToItem(last) }
|
||||
}
|
||||
drawerScope.launch { scrollToBottom(animate = true) }
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user