From 7cf5286bfbc2c2085083f78874d586fa1f5f6c02 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:28:17 +0200 Subject: [PATCH] Fix slow Wear scroll intent detection --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 60 ++++++++++------ .../bitchat/watch/ui/ChatAutoScrollTest.kt | 72 ++++++++++++++++--- 2 files changed, 104 insertions(+), 28 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 15b18436..f304446e 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -50,6 +50,7 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens import com.bitchat.watch.ui.theme.LocalBitchatPalette import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlin.math.sign /** * The shared chat body for global chat and DM threads, following the classic messenger @@ -95,6 +96,7 @@ fun ChatScaffold( val controlsVisible = remember { mutableStateOf(true) } LaunchedEffect(columnState) { var lastPosition = -1 + var scrollIntent = ChatScrollIntentState() snapshotFlow { val first = columnState.layoutInfo.visibleItems.firstOrNull() ChatScrollSnapshot( @@ -103,21 +105,13 @@ fun ChatScaffold( position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0) ) }.collect { snapshot -> - followNewest = updatedFollowNewest( - current = followNewest, + scrollIntent = updatedChatScrollIntent( + current = scrollIntent, snapshot = snapshot, previousPosition = lastPosition ) - if (!snapshot.canScrollForward) { - controlsVisible.value = true - } else if (lastPosition >= 0) { - when { - snapshot.position > lastPosition + CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> - controlsVisible.value = true - snapshot.position < lastPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> - controlsVisible.value = false - } - } + followNewest = scrollIntent.followsNewest + controlsVisible.value = scrollIntent.controlsVisible lastPosition = snapshot.position } } @@ -173,16 +167,42 @@ internal data class ChatScrollSnapshot( val position: Int ) -internal fun updatedFollowNewest( - current: Boolean, +internal data class ChatScrollIntentState( + val followsNewest: Boolean = true, + val controlsVisible: Boolean = true, + val accumulatedDeltaPx: Int = 0 +) + +internal fun updatedChatScrollIntent( + current: ChatScrollIntentState, snapshot: ChatScrollSnapshot, previousPosition: Int -): Boolean = when { - !snapshot.canScrollForward -> true - snapshot.isScrollInProgress && - previousPosition >= 0 && - snapshot.position < previousPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> false - else -> current +): ChatScrollIntentState { + if (!snapshot.canScrollForward) return ChatScrollIntentState() + if (!snapshot.isScrollInProgress || previousPosition < 0) return current + + val delta = snapshot.position - previousPosition + val accumulatedDelta = when { + delta == 0 -> current.accumulatedDeltaPx + current.accumulatedDeltaPx == 0 || + current.accumulatedDeltaPx.sign == delta.sign -> + current.accumulatedDeltaPx + delta + else -> delta + } + val movedAway = accumulatedDelta <= -CHAT_SCROLL_DIRECTION_THRESHOLD_PX + val movedTowardNewest = accumulatedDelta >= CHAT_SCROLL_DIRECTION_THRESHOLD_PX + + return current.copy( + followsNewest = current.followsNewest && !movedAway, + controlsVisible = when { + movedAway -> false + movedTowardNewest -> true + else -> current.controlsVisible + }, + // Keep sub-threshold movement across discrete rotary events. Once intent is clear, + // start a fresh accumulator so reversing direction gets the same threshold treatment. + accumulatedDeltaPx = if (movedAway || movedTowardNewest) 0 else accumulatedDelta + ) } internal suspend fun scrollToNewestAfterItemsMeasured( 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 ad7c687a..7b2d13b9 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt @@ -12,8 +12,8 @@ class ChatAutoScrollTest { @Test fun `new scroll range from appended message keeps follow intent`() { - val updated = updatedFollowNewest( - current = true, + val updated = updatedChatScrollIntent( + current = ChatScrollIntentState(), snapshot = ChatScrollSnapshot( canScrollForward = true, isScrollInProgress = false, @@ -22,13 +22,13 @@ class ChatAutoScrollTest { previousPosition = 100 ) - assertEquals(true, updated) + assertEquals(ChatScrollIntentState(), updated) } @Test fun `user scroll away disables follow until list reaches newest again`() { - val browsingHistory = updatedFollowNewest( - current = true, + val browsingHistory = updatedChatScrollIntent( + current = ChatScrollIntentState(), snapshot = ChatScrollSnapshot( canScrollForward = true, isScrollInProgress = true, @@ -36,9 +36,10 @@ class ChatAutoScrollTest { ), previousPosition = 100 ) - assertFalse(browsingHistory) + assertFalse(browsingHistory.followsNewest) + assertFalse(browsingHistory.controlsVisible) - val dockedAgain = updatedFollowNewest( + val dockedAgain = updatedChatScrollIntent( current = browsingHistory, snapshot = ChatScrollSnapshot( canScrollForward = false, @@ -47,7 +48,62 @@ class ChatAutoScrollTest { ), previousPosition = 60 ) - assertEquals(true, dockedAgain) + assertEquals(ChatScrollIntentState(), dockedAgain) + } + + @Test + fun `slow scroll away accumulates intent across sub-threshold updates`() { + var state = ChatScrollIntentState() + var previousPosition = 100 + + listOf(94, 88, 82, 76).forEach { position -> + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = true, + position = position + ), + previousPosition = previousPosition + ) + previousPosition = position + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = false, + position = position + ), + previousPosition = previousPosition + ) + } + + assertFalse(state.followsNewest) + assertFalse(state.controlsVisible) + assertEquals(0, state.accumulatedDeltaPx) + } + + @Test + fun `slow scroll toward newest reveals controls without restoring follow early`() { + var state = ChatScrollIntentState(followsNewest = false, controlsVisible = false) + var previousPosition = 60 + + listOf(66, 72, 78, 84).forEach { position -> + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = true, + position = position + ), + previousPosition = previousPosition + ) + previousPosition = position + } + + assertFalse(state.followsNewest) + assertEquals(true, state.controlsVisible) + assertEquals(0, state.accumulatedDeltaPx) } @Test