Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,10 @@
"@react-native-menu/menu"
]
}
},
"reanimated": {
"staticFeatureFlags": {
"DISABLE_COMMIT_PAUSING_MECHANISM": true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pair Reanimated commit pausing with the RN flag

With the current Expo build-properties config, the mobile app still builds with the default/stable React Native feature flags, and repo-wide search shows no preventShadowTreeCommitExhaustion or reactNativeReleaseLevel override. Reanimated’s docs for DISABLE_COMMIT_PAUSING_MECHANISM warn that enabling it without React Native’s preventShadowTreeCommitExhaustion can starve React commits and make the app unresponsive, so iOS/New Architecture keyboard flows can trade the missing-animation bug for a worse production freeze.

Useful? React with 👍 / 👎.

}
}
}
56 changes: 2 additions & 54 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,7 @@ import {
import { TouchableOpacity } from "react-native-gesture-handler";
import ImageViewing from "react-native-image-viewing";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
FadeIn,
FadeInUp,
useSharedValue,
withTiming,
type LayoutAnimationsValues,
type SharedValue,
} from "react-native-reanimated";
import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated";
import { useThemeColor } from "../../lib/useThemeColor";
import { useFontFamily } from "../../lib/useFontFamily";
import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
Expand Down Expand Up @@ -117,14 +110,6 @@ function formatMessageTime(input: string): string {
return MESSAGE_TIME_FORMATTER.format(timestamp);
}

// Rows shift when content above them grows (streaming text, work-log folds);
// animating the container position turns those jumps into slides. Applied
// conditionally — see the gated transition in ThreadFeed: while browsing
// history the animation must NOT run, or every estimate→actual size
// correction plays as a visible slide against the instant scroll-offset
// compensation from maintainVisibleContentPosition.
const FEED_ITEM_LAYOUT_DURATION_MS = 180;

// Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's
// classNames. The fold row's min-h-11 (44px) stays taller than its single
// text-sm line at every supported base font size (26px at the 22pt maximum),
Expand Down Expand Up @@ -1434,52 +1419,16 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
},
[props.onHeaderMaterialVisibilityChange],
);
// True while the viewport sits within ~one screen of the list end — the
// only region where layout shifts should animate. Starts true because the
// list opens pinned to the end.
const nearListEnd = useSharedValue(true);

const handleScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
// anchorTopInset, not topContentInset: under automatic insets the list
// rests at contentOffset.y = -headerHeight (the inset lives only in
// UIKit's adjustedContentInset, so topContentInset is 0 here). Add the
// header height back or the material toggles a full header too late.
reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6);
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
nearListEnd.value =
contentSize.height - layoutMeasurement.height - contentOffset.y < layoutMeasurement.height;
},
[reportHeaderMaterialVisibility, anchorTopInset, nearListEnd],
[reportHeaderMaterialVisibility, anchorTopInset],
);

// Gated variant of the 180ms feed layout slide. Instant while browsing
// history: maintainVisibleContentPosition compensates the scroll offset in
// the same frame a row's measured size lands, so an instant reposition is
// invisible — animating it is exactly what made cold upward scrolls slide
// and jump. Near the end the slide stays on: streaming growth and sends
// shift rows at rest, where the animation is the thing preventing a hard
// visual snap.
const feedItemLayoutTransition = useMemo(() => {
return (values: LayoutAnimationsValues) => {
"worklet";
const duration = nearListEnd.value ? FEED_ITEM_LAYOUT_DURATION_MS : 0;
return {
initialValues: {
originX: values.currentOriginX,
originY: values.currentOriginY,
width: values.currentWidth,
height: values.currentHeight,
},
animations: {
originX: withTiming(values.targetOriginX, { duration }),
originY: withTiming(values.targetOriginY, { duration }),
width: withTiming(values.targetWidth, { duration }),
height: withTiming(values.targetHeight, { duration }),
},
};
};
}, [nearListEnd]);
const handleViewportLayout = useCallback((event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width);
const nextHeight = Math.round(event.nativeEvent.layout.height);
Expand Down Expand Up @@ -1817,7 +1766,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
}
: { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })}
{...(anchoredEndSpace ? { anchoredEndSpace } : {})}
itemLayoutAnimation={feedItemLayoutTransition}
// Patched LegendList prop (patches/@legendapp__list@3.2.0.patch):
// lets its scroll math clamp programmatic scrolls to -headerInset
// instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short
Expand Down
Loading