Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1539,10 +1539,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
// The empty↔filled key below remounts the list, which resets its imperative
// content-inset override — and useKeyboardChatComposerInset (mounted above
// the remount boundary) deduplicates by height, so it never re-reports the
// composer inset to the fresh instance. Without this, the remounted list's
// initial scroll-to-end computes with a zero end inset and rests one
// composer-height short of the end. Layout effect: it must land before the
// list's first positioning tick or the one-shot initial scroll misses it.
// composer inset to the fresh instance. Re-report the measured overlay height
// (composer plus any pending approval / user-input card) so the remounted
// list's scroll math gets the true value; on Android the declarative
// contentInset floor below covers the window before this effect lands.
const listMountKey = `${props.threadId}:${props.feed.length === 0 ? "empty" : "filled"}`;
useLayoutEffect(() => {
const bottom = props.contentInsetEndAdjustment.value;
Expand Down Expand Up @@ -1847,6 +1847,19 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
// ThreadDetailScreen); this tells LegendList's scroll math about the
// extra so programmatic end scrolls land at the true resting offset.
contentInsetEndStaticAdjustment={usesNativeAutomaticInsets ? insets.bottom : 0}
// Android: the composer overlay only exists as the keyboard
// integration's animated bottom padding, which the list's scroll
// math cannot see until the inset reports above land — and those
// arrive via runOnJS, racing the remounted list's one-shot initial
// scroll-at-end. Seed the estimated overlay height as a declarative
// contentInset floor: LegendList consumes it in JS math only
// (Android's ScrollView has no native contentInset prop) and the
// first reported override REPLACES it instead of adding to it.
// Not on iOS: there the prop would reach UIKit and inset natively
// on top of the animated padding.
{...(Platform.OS === "android" && !usesNativeAutomaticInsets
? { contentInset: { bottom: bottomContentInset } }
: {})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-glass iOS loses inset floor

Medium Severity

contentInsetEndStaticAdjustment no longer seeds bottomContentInset when automatic insets are off, and the new declarative contentInset seed is Android-only. Pre-glass iOS (usesAutomaticContentInsets false) is left on the remount reportContentInset path alone, which this PR describes as racing the one-shot initial scroll-to-end—so the first open of an uncached thread can still rest one composer-height low there.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9ed20ff. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-glass iOS does not need the floor: this blind spot is Android-specific. In react-native-keyboard-controller's ScrollViewWithBottomPadding, the animated bottom padding is written as the native iOS contentInset prop (animatedProps.contentInset = effective), so UIKit accounts for it during attach and in the scrollable range, and LegendList reads it back as nativeContentInset from scroll events (its base end inset when no contentInset prop is set). Only Android uses the synthetic contentInsetBottom path — the library's own onContentInsetChange doc says it exists because on Android the synthetic inset is not reflected in onScroll events. That is also why the floor must not be passed on iOS: effective adds the contentInset prop on top of the dynamic padding there, which would double the native inset. Pre-glass iOS behavior is byte-for-byte the same as main (remount re-report only), and the original bug never reproduced on iOS.

// The keyboard integration's offset math (end pinning, max scroll)
// must add the same UIKit-added extra, or its keyboard-open end
// targets land one safe-area short of the true resting offset.
Expand Down
Loading