diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt index 7f4bec4..47de3f3 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt @@ -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 {