Fix slow Wear scroll intent detection

This commit is contained in:
callebtc 2026-08-26 17:28:17 +02:00
parent a125da48aa
commit 7cf5286bfb
2 changed files with 104 additions and 28 deletions

View File

@ -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(

View File

@ -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