Skip to content

fix: scroll to the model end offset in doMaintainScrollAtEnd instead of native scrollToEnd#471

Open
mperonnet wants to merge 1 commit into
LegendApp:mainfrom
mperonnet:fix/maintain-scroll-at-end-model-offset
Open

fix: scroll to the model end offset in doMaintainScrollAtEnd instead of native scrollToEnd#471
mperonnet wants to merge 1 commit into
LegendApp:mainfrom
mperonnet:fix/maintain-scroll-at-end-model-offset

Conversation

@mperonnet

@mperonnet mperonnet commented Jun 12, 2026

Copy link
Copy Markdown

Description

doMaintainScrollAtEnd dispatches the native scrollToEnd command (for the non-RTL case), whose target offset is computed natively from contentSize + contentInset at execution time (RCTScrollViewComponentView: offsetY = contentSize.height - bounds.height + contentInset.bottom).

This desyncs from the library's own model whenever a contentInset update is still in flight. The concrete case: the KeyboardAwareLegendList integration with react-native-keyboard-controller, where the composer/keyboard clearance is a contentInset driven by Reanimated animatedProps. On Fabric that inset lands via an asynchronous shadow-tree commit — and Reanimated commits are paused while a React commit is in flight, which is exactly the moment maintainScrollAtEnd fires (a new message was just appended). The native command then computes its target with the stale inset and stops exactly insetEnd short of the model's end position.

Nothing self-corrects afterwards: checkAtBottom computes distanceFromEnd = contentSize - scroll - scrollLength - insetEnd with contentSize itself including insetEnd, so the clamped-short position (real content bottom at viewport bottom, last rows hidden behind the composer) still reads as isAtEnd === true.

Observed symptom in production (RN 0.85.3, Reanimated 4.3.1, New Arch, keyboard-controller 1.21.11, list 3.0.4): on send with the keyboard open, content drops behind the keyboard and then visibly re-adjusts ~100 ms later when the iOS retry (shouldRetryUnalignedEndScroll) re-dispatches after the inset commit lands; on mount, conversations intermittently rest with the last message hidden behind the composer. Verified on device by reading the native inset from scroll events (nativeEvent.contentInset): the model's contentInsetOverride.bottom was 86 while the native inset was still 0 at the time the end scroll executed.

Fix

Dispatch scrollTo at the model-computed end offset (getContentSize(ctx) - scrollLength, clamped to 0) instead of the native scrollToEnd command. This is consistent with every other scroll path in the library — scrollToEnd/scrollToIndex already target model-computed offsets that include getContentInsetEnd — and makes maintainScrollAtEnd independent of native inset timing. The RTL-horizontal branch already worked this way (logical offset → native offset); this aligns the default branch with it.

Additional context

I was able to reproduce and patch this locally, and the fix resolves the issue in my testing.

The diagnosis was done with Claude's assistance. Sharing in case it's useful; feel free to close the issue if this analysis doesn't match the intended behavior.

doMaintainScrollAtEnd used the native scrollToEnd command, whose target
is computed natively from contentSize + contentInset at execution time.
When a contentInset update is still in flight — e.g. an inset driven by
Reanimated animatedProps on Fabric, which lands via an asynchronous
shadow-tree commit (and is paused during React commits, i.e. exactly
when a new message was just appended) — the native command stops short
of the model's end position by exactly the pending inset, and nothing
corrects it afterwards: checkAtBottom subtracts insetEnd back out, so
the clamped position still reads as isAtEnd.

Dispatch scrollTo at the model-computed end offset instead
(getContentSize - scrollLength), consistent with every other scroll
path in the library (scrollToEnd/scrollToIndex already target
model-computed offsets that include getContentInsetEnd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant