From 0e5d2cb5ea754e44bbaab181ce6873bf3d9eaeea Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:55:58 +0000 Subject: [PATCH] fix(mobile): keep a parked chat reader steady while a reply streams (#39209) React Native Web repeats the last scroll event 100 ms after scrolling stops. The page list cleared its record of its own scroll write on the first matching event, so the repeat after each place-keeping correction counted as the reader scrolling. For a parked reader that froze the streaming reply below for 300 ms about every 0.7 s and released it in one step, which showed as lurches of 20-40 px. --- .../mobile-native-chat-reading-place.test.tsx | 15 +++++++++++++++ .../transcript/MobileNativeChatPageList.tsx | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/mobile/src/session/native-chat/mobile-native-chat-reading-place.test.tsx b/mobile/src/session/native-chat/mobile-native-chat-reading-place.test.tsx index 82cbaf45e79..296bf73e68c 100644 --- a/mobile/src/session/native-chat/mobile-native-chat-reading-place.test.tsx +++ b/mobile/src/session/native-chat/mobile-native-chat-reading-place.test.tsx @@ -433,6 +433,21 @@ it('holds back a change below a scrolling reader until they rest, then keeps the expect(chat0.scroller.top(mobileNativeChatRowId('m4'))).toBe(line) }) +it('a parked reader stays parked when React Native Web repeats the scroll event of its own correction', () => { + const rows = Array.from({ length: 12 }, (_, i) => say(`m${i}`, i % 2 ? 'assistant' : 'user', 'x')) + const chat0 = mount(rows) + chat0.read(() => chat0.scroller.showFrom(40)) + const line = chat0.scroller.top(mobileNativeChatRowId('m4')) + const n1 = say('n1', 'assistant', 'reply') + chat0.show([...rows, n1]) + chat0.scrolled() + // React Native Web sends the last scroll event again 100 ms after scrolling stops. + chat0.scrolled() + chat0.show([...rows, n1, say('n2', 'assistant', 'more')]) + expect(chat0.drawn(mobileNativeChatRowId('n2'))).toBe(1) + expect(chat0.scroller.top(mobileNativeChatRowId('m4'))).toBe(line) +}) + it('releases a held change once a chat hidden mid-scroll is shown again and the reader rests', () => { const rows = Array.from({ length: 12 }, (_, i) => say(`m${i}`, i % 2 ? 'assistant' : 'user', 'x')) const chat0 = mount(rows) diff --git a/mobile/src/session/native-chat/transcript/MobileNativeChatPageList.tsx b/mobile/src/session/native-chat/transcript/MobileNativeChatPageList.tsx index 27e36d78ac3..2d8d0e526d8 100644 --- a/mobile/src/session/native-chat/transcript/MobileNativeChatPageList.tsx +++ b/mobile/src/session/native-chat/transcript/MobileNativeChatPageList.tsx @@ -361,8 +361,9 @@ export function MobileNativeChatPageList({ return } const byReader = own.current === null || Math.abs(s.scrollTop - own.current) >= 1 - own.current = null + // RN Web repeats the last scroll event 100 ms after scrolling stops; keep our write so that repeat is ours too. if (byReader) { + own.current = null readerScrolledAt.current = Date.now() } onEndDistance(Math.max(0, -s.scrollTop), byReader)