| ... | @@ -45,14 +45,7 @@ import { | ... | @@ -45,14 +45,7 @@ import { |
| 45 | import { TouchableOpacity } from "react-native-gesture-handler"; | 45 | import { TouchableOpacity } from "react-native-gesture-handler"; |
| 46 | import ImageViewing from "react-native-image-viewing"; | 46 | import ImageViewing from "react-native-image-viewing"; |
| 47 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 47 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 48 | import Animated, { | 48 | import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated"; |
| 49 | FadeIn, | | |
| 50 | FadeInUp, | | |
| 51 | useSharedValue, | | |
| 52 | withTiming, | | |
| 53 | type LayoutAnimationsValues, | | |
| 54 | type SharedValue, | | |
| 55 | } from "react-native-reanimated"; | | |
| 56 | import { useThemeColor } from "../../lib/useThemeColor"; | 49 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 57 | import { useFontFamily } from "../../lib/useFontFamily"; | 50 | import { useFontFamily } from "../../lib/useFontFamily"; |
| 58 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; | 51 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| ... | @@ -117,14 +110,6 @@ function formatMessageTime(input: string): string { | ... | @@ -117,14 +110,6 @@ function formatMessageTime(input: string): string { |
| 117 | return MESSAGE_TIME_FORMATTER.format(timestamp); | 110 | return MESSAGE_TIME_FORMATTER.format(timestamp); |
| 118 | } | 111 | } |
| 119 | | 112 | |
| 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. | | |
| 126 | const FEED_ITEM_LAYOUT_DURATION_MS = 180; | | |
| 127 | | | |
| 128 | // Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's | 113 | // Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's |
| 129 | // classNames. The fold row's min-h-11 (44px) stays taller than its single | 114 | // classNames. The fold row's min-h-11 (44px) stays taller than its single |
| 130 | // text-sm line at every supported base font size (26px at the 22pt maximum), | 115 | // 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) { | ... | @@ -1457,11 +1442,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1457 | }, | 1442 | }, |
| 1458 | [props.onHeaderMaterialVisibilityChange], | 1443 | [props.onHeaderMaterialVisibilityChange], |
| 1459 | ); | 1444 | ); |
| 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 | | | |
| 1465 | const handleScroll = useCallback( | 1445 | const handleScroll = useCallback( |
| 1466 | (event: NativeSyntheticEvent<NativeScrollEvent>) => { | 1446 | (event: NativeSyntheticEvent<NativeScrollEvent>) => { |
| 1467 | // anchorTopInset, not topContentInset: under automatic insets the list | 1447 | // anchorTopInset, not topContentInset: under automatic insets the list |
| ... | @@ -1469,10 +1449,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1469,10 +1449,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1469 | // UIKit's adjustedContentInset, so topContentInset is 0 here). Add the | 1449 | // UIKit's adjustedContentInset, so topContentInset is 0 here). Add the |
| 1470 | // header height back or the material toggles a full header too late. | 1450 | // header height back or the material toggles a full header too late. |
| 1471 | reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6); | 1451 | 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 | | | |
| 1476 | // Latch bookkeeping. LegendList recomputes its inset-aware end distance | 1452 | // Latch bookkeeping. LegendList recomputes its inset-aware end distance |
| 1477 | // before invoking this handler, so getState() is current. Returning to | 1453 | // before invoking this handler, so getState() is current. Returning to |
| 1478 | // the end re-arms follow no matter who scrolled (the user, or our own | 1454 | // 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) { | ... | @@ -1488,7 +1464,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1488 | } | 1464 | } |
| 1489 | } | 1465 | } |
| 1490 | }, | 1466 | }, |
| 1491 | [reportHeaderMaterialVisibility, anchorTopInset, nearListEnd, props.listRef, setEndFollow], | 1467 | [reportHeaderMaterialVisibility, anchorTopInset, props.listRef, setEndFollow], |
| 1492 | ); | 1468 | ); |
| 1493 | const handleScrollBeginDrag = useCallback(() => { | 1469 | const handleScrollBeginDrag = useCallback(() => { |
| 1494 | userScrollSessionRef.current = true; | 1470 | userScrollSessionRef.current = true; |
| ... | @@ -1508,33 +1484,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1508,33 +1484,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1508 | userScrollSessionRef.current = false; | 1484 | userScrollSessionRef.current = false; |
| 1509 | }, []); | 1485 | }, []); |
| 1510 | | 1486 | |
| 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]); | | |
| 1538 | const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { | 1487 | const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { |
| 1539 | const nextWidth = Math.round(event.nativeEvent.layout.width); | 1488 | const nextWidth = Math.round(event.nativeEvent.layout.width); |
| 1540 | const nextHeight = Math.round(event.nativeEvent.layout.height); | 1489 | const nextHeight = Math.round(event.nativeEvent.layout.height); |
| ... | @@ -1886,7 +1835,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1886,7 +1835,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1886 | } | 1835 | } |
| 1887 | : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} | 1836 | : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} |
| 1888 | {...(anchoredEndSpace ? { anchoredEndSpace } : {})} | 1837 | {...(anchoredEndSpace ? { anchoredEndSpace } : {})} |
| 1889 | itemLayoutAnimation={feedItemLayoutTransition} | | |
| 1890 | // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch): | 1838 | // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch): |
| 1891 | // lets its scroll math clamp programmatic scrolls to -headerInset | 1839 | // lets its scroll math clamp programmatic scrolls to -headerInset |
| 1892 | // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short | 1840 | // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short |