package com.suno.android.ui.screens.orpheus import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.ime import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.platform.LocalDensity import com.suno.android.common_data.repos.orpheus.models.OrpheusMessage @Composable @Suppress("MagicNumber") internal fun ChatAutoScrollEffect( listState: LazyListState, messages: List, ) { var userManuallyScrolledUp by remember { mutableStateOf(false) } val isScrollingUp = listState.isScrollingUp() val isAtBottom by remember(listState) { derivedStateOf { val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 1 } } // Track user scroll behavior LaunchedEffect(isScrollingUp, isAtBottom) { when { isScrollingUp -> userManuallyScrolledUp = true isAtBottom -> userManuallyScrolledUp = false } } // Auto-scroll when keyboard appears val density = LocalDensity.current val imeInsets = WindowInsets.ime LaunchedEffect(messages.size) { snapshotFlow { imeInsets.getBottom(density) } .collect { imeBottom -> if (imeBottom > 0 && messages.isNotEmpty()) { userManuallyScrolledUp = false listState.scrollToItem(messages.lastIndex) } } } // Auto-scroll when message content changes (streaming) or clips are added val lastMessage = messages.lastOrNull() LaunchedEffect(messages.size, lastMessage?.content, lastMessage?.generatedClips?.size) { if (!userManuallyScrolledUp && listState.canScrollForward && messages.isNotEmpty()) { listState.scrollToItem(messages.lastIndex, 1000) } } } @Composable private fun LazyListState.isScrollingUp(): Boolean { var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } return remember(this) { derivedStateOf { if (previousIndex != firstVisibleItemIndex) { previousIndex > firstVisibleItemIndex } else { previousScrollOffset > firstVisibleItemScrollOffset }.also { previousIndex = firstVisibleItemIndex previousScrollOffset = firstVisibleItemScrollOffset } } }.value }