mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix last bugs from test #5
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -3,6 +3,7 @@ package ru.fromchat.ui.chat
|
|||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -20,15 +22,19 @@ import androidx.compose.foundation.layout.windowInsetsPadding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -36,10 +42,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SmallFloatingActionButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -50,10 +57,13 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.withFrameNanos
|
import androidx.compose.runtime.withFrameNanos
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -61,6 +71,11 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import ru.fromchat.api.local.messages.formatChatDateSeparator
|
import ru.fromchat.api.local.messages.formatChatDateSeparator
|
||||||
import ru.fromchat.chat_date_today
|
import ru.fromchat.chat_date_today
|
||||||
@@ -68,6 +83,7 @@ import ru.fromchat.chat_date_yesterday
|
|||||||
import ru.fromchat.utils.rememberRegistrationDateFormatStrings
|
import ru.fromchat.utils.rememberRegistrationDateFormatStrings
|
||||||
import com.pr0gramm3r101.utils.resetFocus
|
import com.pr0gramm3r101.utils.resetFocus
|
||||||
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
|
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.HazeProgressive
|
import dev.chrisbanes.haze.HazeProgressive
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
@@ -183,15 +199,49 @@ fun ChatScreen(
|
|||||||
val listState = rememberSaveable(panelId, saver = LazyListState.Saver) {
|
val listState = rememberSaveable(panelId, saver = LazyListState.Saver) {
|
||||||
LazyListState(0, 0)
|
LazyListState(0, 0)
|
||||||
}
|
}
|
||||||
var isNearBottom by rememberSaveable(panelId) { mutableStateOf(true) }
|
val fakeScrollOffset = remember(panelId) { Animatable(0f) }
|
||||||
LaunchedEffect(listState, panelState.messages.size) {
|
LaunchedEffect(panelId) {
|
||||||
|
// #region agent log
|
||||||
|
agentDebugLog(
|
||||||
|
hypothesisId = "H4",
|
||||||
|
location = "ChatScreen.kt:188",
|
||||||
|
message = "chat_screen_opened",
|
||||||
|
data = mapOf(
|
||||||
|
"panelId" to panelId,
|
||||||
|
"listStateHash" to listState.hashCode(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
|
val isNearBottom by remember(panelId) {
|
||||||
|
derivedStateOf { listState.isChatNearBottom() }
|
||||||
|
}
|
||||||
|
LaunchedEffect(listState, panelId) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val minVisibleIndex = listState.layoutInfo.visibleItemsInfo.minOfOrNull { it.index }
|
val info = listState.layoutInfo
|
||||||
?: Int.MAX_VALUE
|
Triple(
|
||||||
minVisibleIndex <= 2
|
info.totalItemsCount,
|
||||||
|
listState.firstVisibleItemIndex,
|
||||||
|
listState.firstVisibleItemScrollOffset,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { nearBottom -> isNearBottom = nearBottom }
|
.collect { (totalItems, firstIndex, firstOffset) ->
|
||||||
|
// #region agent log
|
||||||
|
agentDebugLog(
|
||||||
|
hypothesisId = "H1",
|
||||||
|
location = "ChatScreen.kt:196",
|
||||||
|
message = "scroll_state_changed",
|
||||||
|
data = mapOf(
|
||||||
|
"panelId" to panelId,
|
||||||
|
"totalItems" to totalItems,
|
||||||
|
"firstVisibleItemIndex" to firstIndex,
|
||||||
|
"firstVisibleItemScrollOffset" to firstOffset,
|
||||||
|
"isNearBottom" to listState.isChatNearBottom(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val fallbackMessageHeightPx = remember(density) { with(density) { 80.dp.roundToPx() } }
|
val fallbackMessageHeightPx = remember(density) { with(density) { 80.dp.roundToPx() } }
|
||||||
@@ -671,8 +721,7 @@ fun ChatScreen(
|
|||||||
previousMessageFingerprint = fingerprint
|
previousMessageFingerprint = fingerprint
|
||||||
|
|
||||||
val lastIsOurs = lastMessage?.user_id == currentUserId
|
val lastIsOurs = lastMessage?.user_id == currentUserId
|
||||||
val minVisibleIndex = listState.layoutInfo.visibleItemsInfo.minOfOrNull { it.index } ?: Int.MAX_VALUE
|
val isNearBottom = listState.isChatNearBottom()
|
||||||
val isNearBottom = minVisibleIndex <= 2
|
|
||||||
if (lastIsOurs || isNearBottom) {
|
if (lastIsOurs || isNearBottom) {
|
||||||
if (listState.layoutInfo.visibleItemsInfo.isNotEmpty()) {
|
if (listState.layoutInfo.visibleItemsInfo.isNotEmpty()) {
|
||||||
delay(100.milliseconds)
|
delay(100.milliseconds)
|
||||||
@@ -1062,6 +1111,9 @@ fun ChatScreen(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
translationY = fakeScrollOffset.value
|
||||||
|
}
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.hazeSource(hazeState),
|
.hazeSource(hazeState),
|
||||||
userScrollEnabled = !contextMenuState.isOpen,
|
userScrollEnabled = !contextMenuState.isOpen,
|
||||||
@@ -1296,32 +1348,94 @@ fun ChatScreen(
|
|||||||
item { Spacer(modifier.height(floatingHeaderClearance)) }
|
item { Spacer(modifier.height(floatingHeaderClearance)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val showScrollToBottomFab = panel.usesPublicGroupSubtitle &&
|
val showScrollToBottomFab = !isNearBottom &&
|
||||||
!isNearBottom &&
|
|
||||||
panelState.messages.isNotEmpty() &&
|
panelState.messages.isNotEmpty() &&
|
||||||
!contextMenuState.isOpen
|
!contextMenuState.isOpen
|
||||||
|
LaunchedEffect(showScrollToBottomFab) {
|
||||||
|
// #region agent log
|
||||||
|
agentDebugLog(
|
||||||
|
hypothesisId = "H2",
|
||||||
|
location = "ChatScreen.kt:1338",
|
||||||
|
message = "show_scroll_button_changed",
|
||||||
|
data = mapOf(
|
||||||
|
"panelId" to panelId,
|
||||||
|
"showScrollToBottomFab" to showScrollToBottomFab,
|
||||||
|
"isNearBottom" to isNearBottom,
|
||||||
|
"messageCount" to panelState.messages.size,
|
||||||
|
"contextMenuOpen" to contextMenuState.isOpen,
|
||||||
|
"usesPublicGroupSubtitle" to panel.usesPublicGroupSubtitle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showScrollToBottomFab,
|
visible = showScrollToBottomFab,
|
||||||
enter = fadeIn(tween(150)) +
|
enter = fadeIn(
|
||||||
expandVertically(expandFrom = Alignment.Bottom),
|
animationSpec = tween(
|
||||||
exit = fadeOut(tween(120)) +
|
durationMillis = 260,
|
||||||
shrinkVertically(shrinkTowards = Alignment.Bottom),
|
easing = FastOutSlowInEasing,
|
||||||
|
),
|
||||||
|
) + scaleIn(
|
||||||
|
initialScale = 0.7f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 260,
|
||||||
|
easing = FastOutSlowInEasing,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
exit = fadeOut(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
easing = FastOutSlowInEasing,
|
||||||
|
),
|
||||||
|
) + scaleOut(
|
||||||
|
targetScale = 0.7f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
easing = FastOutSlowInEasing,
|
||||||
|
),
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 16.dp, bottom = 16.dp),
|
.zIndex(2f)
|
||||||
|
.padding(
|
||||||
|
end = 13.dp,
|
||||||
|
bottom = innerPadding.calculateBottomPadding() + 12.dp,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
SmallFloatingActionButton(
|
ChatScrollToBottomButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch { listState.animateScrollToItem(0) }
|
scope.launch {
|
||||||
|
// #region agent log
|
||||||
|
agentDebugLog(
|
||||||
|
hypothesisId = "H5",
|
||||||
|
location = "ChatScreen.kt:1402",
|
||||||
|
message = "scroll_button_jump_to_bottom",
|
||||||
|
data = mapOf(
|
||||||
|
"panelId" to panelId,
|
||||||
|
"fromIndex" to listState.firstVisibleItemIndex,
|
||||||
|
"fromOffset" to listState.firstVisibleItemScrollOffset,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// Snap list to bottom first so content is correct.
|
||||||
|
listState.scrollChatToBottom()
|
||||||
|
|
||||||
|
// Fake a short smooth scroll by translating the whole chat
|
||||||
|
// surface from a small offset back to rest.
|
||||||
|
fakeScrollOffset.snapTo(64f)
|
||||||
|
fakeScrollOffset.animateTo(
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 260,
|
||||||
|
easing = FastOutSlowInEasing,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
contentDescription = scrollToBottomCd,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
hazeState = hazeState,
|
||||||
) {
|
)
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.KeyboardArrowDown,
|
|
||||||
contentDescription = scrollToBottomCd,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatTopBar(
|
ChatTopBar(
|
||||||
@@ -1506,6 +1620,115 @@ private fun ChatDateSeparator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ChatScrollToBottomButtonSize = 44.dp
|
||||||
|
private val ChatScrollToBottomIconSize = 24.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChatScrollToBottomButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
contentDescription: String,
|
||||||
|
hazeState: HazeState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val hazeStyle = rememberChatSurfaceContainerHazeStyle()
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(ChatScrollToBottomButtonSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
// #region agent log
|
||||||
|
agentDebugLog(
|
||||||
|
hypothesisId = "H3",
|
||||||
|
location = "ChatScreen.kt:1519",
|
||||||
|
message = "scroll_button_clicked",
|
||||||
|
data = mapOf(
|
||||||
|
"contentDescription" to contentDescription,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.91f))
|
||||||
|
.hazeEffect(state = hazeState, style = hazeStyle),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.KeyboardArrowDown,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.size(ChatScrollToBottomIconSize),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun agentDebugLog(
|
||||||
|
hypothesisId: String,
|
||||||
|
location: String,
|
||||||
|
message: String,
|
||||||
|
data: Map<String, Any?> = emptyMap(),
|
||||||
|
runId: String = "initial",
|
||||||
|
) {
|
||||||
|
val timestamp = kotlin.time.Clock.System.now().toEpochMilliseconds()
|
||||||
|
fun escape(value: String): String {
|
||||||
|
return value
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataJson = buildString {
|
||||||
|
append("{")
|
||||||
|
data.entries.joinToString(",") { (key, rawValue) ->
|
||||||
|
val value = rawValue?.toString() ?: "null"
|
||||||
|
"\"${escape(key)}\":\"${escape(value)}\""
|
||||||
|
}.let { append(it) }
|
||||||
|
append("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = buildString {
|
||||||
|
append("{")
|
||||||
|
append("\"sessionId\":\"a042f7\",")
|
||||||
|
append("\"id\":\"log_")
|
||||||
|
append(timestamp)
|
||||||
|
append("_")
|
||||||
|
append(escape(hypothesisId))
|
||||||
|
append("\",")
|
||||||
|
append("\"timestamp\":")
|
||||||
|
append(timestamp)
|
||||||
|
append(",")
|
||||||
|
append("\"location\":\"")
|
||||||
|
append(escape(location))
|
||||||
|
append("\",")
|
||||||
|
append("\"message\":\"")
|
||||||
|
append(escape(message))
|
||||||
|
append("\",")
|
||||||
|
append("\"runId\":\"")
|
||||||
|
append(escape(runId))
|
||||||
|
append("\",")
|
||||||
|
append("\"hypothesisId\":\"")
|
||||||
|
append(escape(hypothesisId))
|
||||||
|
append("\",")
|
||||||
|
append("\"data\":")
|
||||||
|
append(dataJson)
|
||||||
|
append("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d("AgentDebug", json)
|
||||||
|
|
||||||
|
kotlinx.coroutines.GlobalScope.launch {
|
||||||
|
runCatching {
|
||||||
|
ApiClient.http.post("http://192.168.1.6:7629/ingest/edf4b1c4-ae73-4d46-9110-6235fc587a70") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
header("X-Debug-Session-Id", "a042f7")
|
||||||
|
setBody(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun LazyListState.scrollChatMessageToCenter(
|
private suspend fun LazyListState.scrollChatMessageToCenter(
|
||||||
lazyIndex: Int,
|
lazyIndex: Int,
|
||||||
topClearancePx: Int,
|
topClearancePx: Int,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.fromchat.ui.chat
|
package ru.fromchat.ui.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -249,6 +250,19 @@ fun buildChatListItems(
|
|||||||
return chronological.asReversed()
|
return chronological.asReversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether a reversed chat list is showing the latest messages (index 0 = bottom spacer). */
|
||||||
|
fun LazyListState.isChatNearBottom(threshold: Int = 2): Boolean {
|
||||||
|
if (layoutInfo.totalItemsCount == 0) return true
|
||||||
|
return firstVisibleItemIndex <= threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scrolls a reversed chat list to the visual bottom (newest messages). */
|
||||||
|
suspend fun LazyListState.scrollChatToBottom() {
|
||||||
|
if (layoutInfo.totalItemsCount == 0) return
|
||||||
|
// Fallback: direct snap to bottom spacer; animation is handled by button enter/exit.
|
||||||
|
scrollToItem(0)
|
||||||
|
}
|
||||||
|
|
||||||
/** LazyColumn index for a message (index 0 = bottom spacer). */
|
/** LazyColumn index for a message (index 0 = bottom spacer). */
|
||||||
fun lazyIndexForMessageId(listItems: List<ChatListItem>, messageId: Int): Int? {
|
fun lazyIndexForMessageId(listItems: List<ChatListItem>, messageId: Int): Int? {
|
||||||
val itemIndex = listItems.indexOfFirst {
|
val itemIndex = listItems.indexOfFirst {
|
||||||
|
|||||||
@@ -43,4 +43,3 @@ fun DisplayName(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user