From 44b2b8cb79c62d728945c1a357ef9f201b47ae28 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:32:20 +0200 Subject: [PATCH] Fix first Wear message auto-scroll --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 28 +++++++++++-- .../bitchat/watch/ui/ChatAutoScrollTest.kt | 42 ++++++++++++++++--- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 3404881b..0bcdfed3 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -113,9 +113,21 @@ fun ChatScaffold( val followNewest = remember(messages.size) { atNewest } LaunchedEffect(columnState, messages.size) { if (messages.isNotEmpty() && followNewest) { + val expectedSingleMessageKey = messages.singleOrNull()?.id scrollToNewestAfterItemsMeasured( expectedItemCount = messages.size, - measuredItemCounts = snapshotFlow { columnState.layoutInfo.totalItemsCount } + 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. @@ -138,12 +150,22 @@ fun ChatScaffold( ) } +internal data class MeasuredChatLayout( + val itemCount: Int, + val singleVisibleItemKey: Any? +) + internal suspend fun scrollToNewestAfterItemsMeasured( expectedItemCount: Int, - measuredItemCounts: Flow, + expectedSingleMessageKey: Any?, + measuredLayouts: Flow, scrollToEnd: suspend () -> Unit ) { - measuredItemCounts.first { it >= expectedItemCount } + measuredLayouts.first { layout -> + layout.itemCount >= expectedItemCount && + (expectedSingleMessageKey == null || + layout.singleVisibleItemKey == expectedSingleMessageKey) + } scrollToEnd() } diff --git a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt index 2262bc75..0b3f85b5 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt @@ -12,13 +12,14 @@ class ChatAutoScrollTest { @Test fun `newest scroll waits until appended message is measured`() = runTest { - val measuredItemCount = MutableStateFlow(3) + val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null)) var scrollCount = 0 val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) { scrollToNewestAfterItemsMeasured( expectedItemCount = 4, - measuredItemCounts = measuredItemCount + expectedSingleMessageKey = null, + measuredLayouts = measuredLayouts ) { scrollCount += 1 } @@ -27,7 +28,7 @@ class ChatAutoScrollTest { assertFalse(scrollJob.isCompleted) assertEquals(0, scrollCount) - measuredItemCount.value = 4 + measuredLayouts.value = MeasuredChatLayout(4, null) scrollJob.join() assertEquals(1, scrollCount) @@ -35,16 +36,47 @@ class ChatAutoScrollTest { @Test fun `newest scroll runs immediately when messages are already measured`() = runTest { - val measuredItemCount = MutableStateFlow(4) + val measuredLayouts = MutableStateFlow(MeasuredChatLayout(4, null)) var scrollCount = 0 scrollToNewestAfterItemsMeasured( expectedItemCount = 4, - measuredItemCounts = measuredItemCount + 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) + } }