Fix Wear chat auto-scroll timing

This commit is contained in:
callebtc 2026-07-31 17:51:16 +02:00
parent cb6d68958d
commit efeb9b54b5
2 changed files with 73 additions and 4 deletions

View File

@ -42,6 +42,8 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.watch.ui.theme.BitchatMotion
import com.bitchat.watch.ui.theme.ChatVisualTokens
import com.bitchat.watch.ui.theme.LocalBitchatPalette
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
/**
* The shared chat body for global chat and DM threads, following the classic messenger
@ -106,11 +108,19 @@ fun ChatScaffold(
}
// Stick to bottom: follow new messages while resting at the newest.
// Capture this when the message count changes, before the new layout can temporarily make
// canScrollForward true and report that the user is browsing history.
val followNewest = remember(messages.size) { atNewest }
LaunchedEffect(columnState, messages.size) {
if (messages.isNotEmpty() && atNewest) {
// scrollBy to the end of the range: animateScrollToItem stops as soon as the
// item is partially visible, which left the last message cropped.
columnState.scroll { scrollBy(Float.MAX_VALUE) }
if (messages.isNotEmpty() && followNewest) {
scrollToNewestAfterItemsMeasured(
expectedItemCount = messages.size,
measuredItemCounts = snapshotFlow { columnState.layoutInfo.totalItemsCount }
) {
// scrollBy to the end of the range: animateScrollToItem stops as soon as the
// item is partially visible, which left the last message cropped.
columnState.scroll { scrollBy(Float.MAX_VALUE) }
}
}
}
@ -128,6 +138,15 @@ fun ChatScaffold(
)
}
internal suspend fun scrollToNewestAfterItemsMeasured(
expectedItemCount: Int,
measuredItemCounts: Flow<Int>,
scrollToEnd: suspend () -> Unit
) {
measuredItemCounts.first { it >= expectedItemCount }
scrollToEnd()
}
@Composable
private fun ChatBody(
messages: List<BitchatMessage>,

View File

@ -0,0 +1,50 @@
package com.bitchat.watch.ui
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class ChatAutoScrollTest {
@Test
fun `newest scroll waits until appended message is measured`() = runTest {
val measuredItemCount = MutableStateFlow(3)
var scrollCount = 0
val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) {
scrollToNewestAfterItemsMeasured(
expectedItemCount = 4,
measuredItemCounts = measuredItemCount
) {
scrollCount += 1
}
}
assertFalse(scrollJob.isCompleted)
assertEquals(0, scrollCount)
measuredItemCount.value = 4
scrollJob.join()
assertEquals(1, scrollCount)
}
@Test
fun `newest scroll runs immediately when messages are already measured`() = runTest {
val measuredItemCount = MutableStateFlow(4)
var scrollCount = 0
scrollToNewestAfterItemsMeasured(
expectedItemCount = 4,
measuredItemCounts = measuredItemCount
) {
scrollCount += 1
}
assertEquals(1, scrollCount)
}
}