fix(chat): 新消息自动跟随底部(微信/QQ 式)

reverseLayout 会锚定原可见项,导致新消息插到视口外。改为:滚动结束时记录是否贴底(容差 64px),最新一项变化且处于跟随状态时 animateScrollToItem(0);用户上滑查看历史时不再强制滚动。
This commit is contained in:
AskaEth committed 2026-09-08 21:16:49 +08:00
1 parent 67ed0479e4
commit e04995a84d
1 file changed
+28
@@ -18,6 +18,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -40,6 +43,9 @@ import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Sidebar
import top.yukonga.miuix.kmp.theme.MiuixTheme
/** 判定「贴着底部」的容差(px)。 */
private const val BOTTOM_TOLERANCE = 64
@Composable
fun ChatScreen(
container: AppContainer,
@@ -52,6 +58,28 @@ fun ChatScreen(
val state by viewModel.state.collectAsState()
val listState = rememberLazyListState()
// 是否跟随底部(微信/QQ 式):只有用户主动滚到别处时才停止跟随。
// 只在「滚动结束」时更新,避免新消息插入导致的锚点位移把状态带偏。
var followBottom by remember { mutableStateOf(true) }
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.distinctUntilChanged()
.collect { scrolling ->
if (!scrolling) {
followBottom = listState.firstVisibleItemIndex == 0 &&
listState.firstVisibleItemScrollOffset < BOTTOM_TOLERANCE
}
}
}
// 最新一项变化(新消息 / 乐观气泡 / 流式气泡 / 落库归一)时,跟随则滚到底。
val newestKey = state.items.lastOrNull()?.key
LaunchedEffect(newestKey) {
if (newestKey != null && followBottom) {
listState.animateScrollToItem(0)
}
}
// reverseLayout 下「列表末尾」就是屏幕顶部;接近末尾时加载更早的一页。
LaunchedEffect(listState) {
snapshotFlow {