mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
Merge pull request #844 from permissionlesstech/codex/fix-wear-chat-auto-scroll
Fix Wear chat auto-scroll timing
This commit is contained in:
commit
e07a38f634
@ -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,31 @@ 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) {
|
||||
val expectedSingleMessageKey = messages.singleOrNull()?.id
|
||||
scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount = messages.size,
|
||||
expectedSingleMessageKey = expectedSingleMessageKey,
|
||||
measuredLayouts = snapshotFlow {
|
||||
val layoutInfo = columnState.layoutInfo
|
||||
MeasuredChatLayout(
|
||||
itemCount = layoutInfo.totalItemsCount,
|
||||
singleVisibleItemKey = if (expectedSingleMessageKey != null) {
|
||||
layoutInfo.visibleItems.singleOrNull()?.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
// 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 +150,25 @@ fun ChatScaffold(
|
||||
)
|
||||
}
|
||||
|
||||
internal data class MeasuredChatLayout(
|
||||
val itemCount: Int,
|
||||
val singleVisibleItemKey: Any?
|
||||
)
|
||||
|
||||
internal suspend fun scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount: Int,
|
||||
expectedSingleMessageKey: Any?,
|
||||
measuredLayouts: Flow<MeasuredChatLayout>,
|
||||
scrollToEnd: suspend () -> Unit
|
||||
) {
|
||||
measuredLayouts.first { layout ->
|
||||
layout.itemCount >= expectedItemCount &&
|
||||
(expectedSingleMessageKey == null ||
|
||||
layout.singleVisibleItemKey == expectedSingleMessageKey)
|
||||
}
|
||||
scrollToEnd()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatBody(
|
||||
messages: List<BitchatMessage>,
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
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 measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null))
|
||||
var scrollCount = 0
|
||||
|
||||
val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount = 4,
|
||||
expectedSingleMessageKey = null,
|
||||
measuredLayouts = measuredLayouts
|
||||
) {
|
||||
scrollCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(scrollJob.isCompleted)
|
||||
assertEquals(0, scrollCount)
|
||||
|
||||
measuredLayouts.value = MeasuredChatLayout(4, null)
|
||||
scrollJob.join()
|
||||
|
||||
assertEquals(1, scrollCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `newest scroll runs immediately when messages are already measured`() = runTest {
|
||||
val measuredLayouts = MutableStateFlow(MeasuredChatLayout(4, null))
|
||||
var scrollCount = 0
|
||||
|
||||
scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount = 4,
|
||||
expectedSingleMessageKey = null,
|
||||
measuredLayouts = measuredLayouts
|
||||
) {
|
||||
scrollCount += 1
|
||||
}
|
||||
|
||||
assertEquals(1, scrollCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `first message waits past stale empty placeholder layout`() = runTest {
|
||||
val messageKey = "first-message"
|
||||
val measuredLayouts = MutableStateFlow(
|
||||
MeasuredChatLayout(itemCount = 1, singleVisibleItemKey = "empty-placeholder")
|
||||
)
|
||||
var scrollCount = 0
|
||||
|
||||
val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount = 1,
|
||||
expectedSingleMessageKey = messageKey,
|
||||
measuredLayouts = measuredLayouts
|
||||
) {
|
||||
scrollCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(scrollJob.isCompleted)
|
||||
assertEquals(0, scrollCount)
|
||||
|
||||
measuredLayouts.value = MeasuredChatLayout(
|
||||
itemCount = 1,
|
||||
singleVisibleItemKey = messageKey
|
||||
)
|
||||
scrollJob.join()
|
||||
|
||||
assertEquals(1, scrollCount)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user