authorgravatar for github@jayz.usJay Meistrich <github@jayz.us> 2026-08-07 10:40:06+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 09:40:06+00:00
log8100062a78f1e1942663a0cea9ddc42d2f835525
treee3eda4dba1029bd5ac67fba0ec9d69e7568b01fc
parenta1762fdd7482728800f1f4fd260c632f01a30b1f
signature Signed by PGP key B5690EEEBB952194

fix(mobile): improve keyboard avoiding (#5451)


2 files changed, 7 insertions(+), 54 deletions(-)

apps/mobile/package.json+5
......@@ -138,5 +138,10 @@
138138 "@react-native-menu/menu"
139139 ]
140140 }
141 },
142 "reanimated": {
143 "staticFeatureFlags": {
144 "DISABLE_COMMIT_PAUSING_MECHANISM": true
145 }
141146 }
142147}
apps/mobile/src/features/threads/ThreadFeed.tsx+2-54
......@@ -45,14 +45,7 @@ import {
4545import { TouchableOpacity } from "react-native-gesture-handler";
4646import ImageViewing from "react-native-image-viewing";
4747import { useSafeAreaInsets } from "react-native-safe-area-context";
48import Animated, {
49 FadeIn,
50 FadeInUp,
51 useSharedValue,
52 withTiming,
53 type LayoutAnimationsValues,
54 type SharedValue,
55} from "react-native-reanimated";
48import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated";
5649import { useThemeColor } from "../../lib/useThemeColor";
5750import { useFontFamily } from "../../lib/useFontFamily";
5851import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
......@@ -117,14 +110,6 @@ function formatMessageTime(input: string): string {
117110 return MESSAGE_TIME_FORMATTER.format(timestamp);
118111}
119112
120// Rows shift when content above them grows (streaming text, work-log folds);
121// animating the container position turns those jumps into slides. Applied
122// conditionally — see the gated transition in ThreadFeed: while browsing
123// history the animation must NOT run, or every estimate→actual size
124// correction plays as a visible slide against the instant scroll-offset
125// compensation from maintainVisibleContentPosition.
126const FEED_ITEM_LAYOUT_DURATION_MS = 180;
127
128113// Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's
129114// classNames. The fold row's min-h-11 (44px) stays taller than its single
130115// text-sm line at every supported base font size (26px at the 22pt maximum),
......@@ -1457,11 +1442,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
14571442 },
14581443 [props.onHeaderMaterialVisibilityChange],
14591444 );
1460 // True while the viewport sits within ~one screen of the list end — the
1461 // only region where layout shifts should animate. Starts true because the
1462 // list opens pinned to the end.
1463 const nearListEnd = useSharedValue(true);
1464
14651445 const handleScroll = useCallback(
14661446 (event: NativeSyntheticEvent<NativeScrollEvent>) => {
14671447 // anchorTopInset, not topContentInset: under automatic insets the list
......@@ -1469,10 +1449,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
14691449 // UIKit's adjustedContentInset, so topContentInset is 0 here). Add the
14701450 // header height back or the material toggles a full header too late.
14711451 reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6);
1472 const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
1473 nearListEnd.value =
1474 contentSize.height - layoutMeasurement.height - contentOffset.y < layoutMeasurement.height;
1475
14761452 // Latch bookkeeping. LegendList recomputes its inset-aware end distance
14771453 // before invoking this handler, so getState() is current. Returning to
14781454 // the end re-arms follow no matter who scrolled (the user, or our own
......@@ -1488,7 +1464,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
14881464 }
14891465 }
14901466 },
1491 [reportHeaderMaterialVisibility, anchorTopInset, nearListEnd, props.listRef, setEndFollow],
1467 [reportHeaderMaterialVisibility, anchorTopInset, props.listRef, setEndFollow],
14921468 );
14931469 const handleScrollBeginDrag = useCallback(() => {
14941470 userScrollSessionRef.current = true;
......@@ -1508,33 +1484,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
15081484 userScrollSessionRef.current = false;
15091485 }, []);
15101486
1511 // Gated variant of the 180ms feed layout slide. Instant while browsing
1512 // history: maintainVisibleContentPosition compensates the scroll offset in
1513 // the same frame a row's measured size lands, so an instant reposition is
1514 // invisible — animating it is exactly what made cold upward scrolls slide
1515 // and jump. Near the end the slide stays on: streaming growth and sends
1516 // shift rows at rest, where the animation is the thing preventing a hard
1517 // visual snap.
1518 const feedItemLayoutTransition = useMemo(() => {
1519 return (values: LayoutAnimationsValues) => {
1520 "worklet";
1521 const duration = nearListEnd.value ? FEED_ITEM_LAYOUT_DURATION_MS : 0;
1522 return {
1523 initialValues: {
1524 originX: values.currentOriginX,
1525 originY: values.currentOriginY,
1526 width: values.currentWidth,
1527 height: values.currentHeight,
1528 },
1529 animations: {
1530 originX: withTiming(values.targetOriginX, { duration }),
1531 originY: withTiming(values.targetOriginY, { duration }),
1532 width: withTiming(values.targetWidth, { duration }),
1533 height: withTiming(values.targetHeight, { duration }),
1534 },
1535 };
1536 };
1537 }, [nearListEnd]);
15381487 const handleViewportLayout = useCallback((event: LayoutChangeEvent) => {
15391488 const nextWidth = Math.round(event.nativeEvent.layout.width);
15401489 const nextHeight = Math.round(event.nativeEvent.layout.height);
......@@ -1886,7 +1835,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
18861835 }
18871836 : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })}
18881837 {...(anchoredEndSpace ? { anchoredEndSpace } : {})}
1889 itemLayoutAnimation={feedItemLayoutTransition}
18901838 // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch):
18911839 // lets its scroll math clamp programmatic scrolls to -headerInset
18921840 // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short