| ... | @@ -76,7 +76,11 @@ import { | ... | @@ -76,7 +76,11 @@ import { |
| 76 | } from "../review/nativeReviewDiffAdapter"; | 76 | } from "../review/nativeReviewDiffAdapter"; |
| 77 | import { buildReviewParsedDiff } from "../review/reviewModel"; | 77 | import { buildReviewParsedDiff } from "../review/reviewModel"; |
| 78 | import { cn } from "../../lib/cn"; | 78 | import { cn } from "../../lib/cn"; |
| 79 | import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; | 79 | import { |
| | 80 | deriveCenteredContentHorizontalPadding, |
| | 81 | deriveThreadFeedInitialContentInset, |
| | 82 | type LayoutVariant, |
| | 83 | } from "../../lib/layout"; |
| 80 | import { | 84 | import { |
| 81 | resolveMarkdownFontSizes, | 85 | resolveMarkdownFontSizes, |
| 82 | resolveNativeMarkdownTypography, | 86 | resolveNativeMarkdownTypography, |
| ... | @@ -1555,6 +1559,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1555,6 +1559,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1555 | const bottomContentInset = props.contentBottomInset ?? 18; | 1559 | const bottomContentInset = props.contentBottomInset ?? 18; |
| 1556 | const usesNativeAutomaticInsets = | 1560 | const usesNativeAutomaticInsets = |
| 1557 | props.usesAutomaticContentInsets === true && Platform.OS === "ios"; | 1561 | props.usesAutomaticContentInsets === true && Platform.OS === "ios"; |
| | 1562 | const initialContentInset = deriveThreadFeedInitialContentInset({ |
| | 1563 | platform: Platform.OS, |
| | 1564 | usesNativeAutomaticInsets, |
| | 1565 | bottomContentInset, |
| | 1566 | }); |
| 1558 | // With automatic insets the header inset lives in UIKit's adjustedContentInset, | 1567 | // With automatic insets the header inset lives in UIKit's adjustedContentInset, |
| 1559 | // which LegendList's JS anchoring math cannot see — it measures the anchored | 1568 | // which LegendList's JS anchoring math cannot see — it measures the anchored |
| 1560 | // end space from the scroll view's frame top. Fold the header height back into | 1569 | // end space from the scroll view's frame top. Fold the header height back into |
| ... | @@ -1794,10 +1803,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1794,10 +1803,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1794 | // The empty↔filled key below remounts the list, which resets its imperative | 1803 | // The empty↔filled key below remounts the list, which resets its imperative |
| 1795 | // content-inset override — and useKeyboardChatComposerInset (mounted above | 1804 | // content-inset override — and useKeyboardChatComposerInset (mounted above |
| 1796 | // the remount boundary) deduplicates by height, so it never re-reports the | 1805 | // the remount boundary) deduplicates by height, so it never re-reports the |
| 1797 | // composer inset to the fresh instance. Without this, the remounted list's | 1806 | // composer inset to the fresh instance. Re-report the measured overlay height |
| 1798 | // initial scroll-to-end computes with a zero end inset and rests one | 1807 | // (composer plus any pending approval / user-input card) so the remounted |
| 1799 | // composer-height short of the end. Layout effect: it must land before the | 1808 | // list's scroll math gets the true value; on Android the declarative |
| 1800 | // list's first positioning tick or the one-shot initial scroll misses it. | 1809 | // contentInset floor below covers the window before this effect lands. |
| 1801 | const listMountKey = `${feedThreadKey}:${props.feed.length === 0 ? "empty" : "filled"}`; | 1810 | const listMountKey = `${feedThreadKey}:${props.feed.length === 0 ? "empty" : "filled"}`; |
| 1802 | useLayoutEffect(() => { | 1811 | useLayoutEffect(() => { |
| 1803 | const bottom = props.contentInsetEndAdjustment.value; | 1812 | const bottom = props.contentInsetEndAdjustment.value; |
| ... | @@ -2104,6 +2113,17 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -2104,6 +2113,17 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 2104 | // ThreadDetailScreen); this tells LegendList's scroll math about the | 2113 | // ThreadDetailScreen); this tells LegendList's scroll math about the |
| 2105 | // extra so programmatic end scrolls land at the true resting offset. | 2114 | // extra so programmatic end scrolls land at the true resting offset. |
| 2106 | contentInsetEndStaticAdjustment={usesNativeAutomaticInsets ? insets.bottom : 0} | 2115 | contentInsetEndStaticAdjustment={usesNativeAutomaticInsets ? insets.bottom : 0} |
| | 2116 | // Android: the composer overlay only exists as the keyboard |
| | 2117 | // integration's animated bottom padding, which the list's scroll |
| | 2118 | // math cannot see until the inset reports above land — and those |
| | 2119 | // arrive via runOnJS, racing the remounted list's one-shot initial |
| | 2120 | // scroll-at-end. Seed the estimated overlay height as a declarative |
| | 2121 | // contentInset floor: LegendList consumes it in JS math only |
| | 2122 | // (Android's ScrollView has no native contentInset prop) and the |
| | 2123 | // first reported override REPLACES it instead of adding to it. |
| | 2124 | // Not on iOS: there the prop would reach UIKit and inset natively |
| | 2125 | // on top of the animated padding. |
| | 2126 | {...(initialContentInset ? { contentInset: initialContentInset } : {})} |
| 2107 | // The keyboard integration's offset math (end pinning, max scroll) | 2127 | // The keyboard integration's offset math (end pinning, max scroll) |
| 2108 | // must add the same UIKit-added extra, or its keyboard-open end | 2128 | // must add the same UIKit-added extra, or its keyboard-open end |
| 2109 | // targets land one safe-area short of the true resting offset. | 2129 | // targets land one safe-area short of the true resting offset. |