Optimize agent stream rendering
This commit is contained in:
@@ -79,6 +79,30 @@ import { isWeb } from "@/constants/platform";
|
||||
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
||||
const isToolSequenceItem = (item?: StreamItem) =>
|
||||
item?.kind === "tool_call" || item?.kind === "thought" || item?.kind === "todo_list";
|
||||
|
||||
const isSameAssistantBlockGroup = (params: {
|
||||
item: StreamItem | null | undefined;
|
||||
other: StreamItem | null | undefined;
|
||||
}) =>
|
||||
params.item?.kind === "assistant_message" &&
|
||||
params.other?.kind === "assistant_message" &&
|
||||
params.item.blockGroupId !== undefined &&
|
||||
params.item.blockGroupId === params.other.blockGroupId;
|
||||
|
||||
const isAssistantBlockFragment = (params: {
|
||||
item: StreamItem;
|
||||
aboveItem: StreamItem | null | undefined;
|
||||
belowItem: StreamItem | null | undefined;
|
||||
}) =>
|
||||
params.item.kind === "assistant_message" &&
|
||||
(isSameAssistantBlockGroup({
|
||||
item: params.item,
|
||||
other: params.aboveItem,
|
||||
}) ||
|
||||
isSameAssistantBlockGroup({
|
||||
item: params.item,
|
||||
other: params.belowItem,
|
||||
}));
|
||||
export interface AgentStreamViewHandle {
|
||||
scrollToBottom(reason?: BottomAnchorLocalRequest["reason"]): void;
|
||||
prepareForViewportChange(): void;
|
||||
@@ -249,6 +273,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
}
|
||||
|
||||
const tightGap = theme.spacing[1]; // 4px
|
||||
const assistantBlockGap = theme.spacing[3]; // 12px
|
||||
const looseGap = theme.spacing[4]; // 16px
|
||||
|
||||
const getGapBetween = useCallback(
|
||||
@@ -261,26 +286,23 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
return tightGap;
|
||||
}
|
||||
if (isToolSequenceItem(item) && isToolSequenceItem(belowItem)) {
|
||||
return tightGap;
|
||||
return 0;
|
||||
}
|
||||
if (item.kind === "user_message" && isToolSequenceItem(belowItem)) {
|
||||
return looseGap;
|
||||
}
|
||||
if (
|
||||
(item.kind === "user_message" || item.kind === "assistant_message") &&
|
||||
isToolSequenceItem(belowItem)
|
||||
) {
|
||||
return tightGap;
|
||||
}
|
||||
if (item.kind === "todo_list" && isToolSequenceItem(belowItem)) {
|
||||
if (item.kind === "assistant_message" && isToolSequenceItem(belowItem)) {
|
||||
return tightGap;
|
||||
}
|
||||
if (isToolSequenceItem(item) && belowItem.kind === "assistant_message") {
|
||||
return tightGap;
|
||||
return looseGap;
|
||||
}
|
||||
if (isSameAssistantBlockGroup({ item, other: belowItem })) {
|
||||
return assistantBlockGap;
|
||||
}
|
||||
return looseGap;
|
||||
},
|
||||
[looseGap, tightGap],
|
||||
[assistantBlockGap, looseGap, tightGap],
|
||||
);
|
||||
|
||||
const renderStreamItemContent = useCallback(
|
||||
@@ -335,7 +357,29 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
);
|
||||
}
|
||||
|
||||
case "assistant_message":
|
||||
case "assistant_message": {
|
||||
const aboveItem =
|
||||
getStreamNeighborItem({
|
||||
strategy: streamRenderStrategy,
|
||||
items,
|
||||
index,
|
||||
relation: "above",
|
||||
}) ??
|
||||
seamAboveItem ??
|
||||
undefined;
|
||||
const belowItem = getStreamNeighborItem({
|
||||
strategy: streamRenderStrategy,
|
||||
items,
|
||||
index,
|
||||
relation: "below",
|
||||
});
|
||||
const spacing = isAssistantBlockFragment({
|
||||
item,
|
||||
aboveItem,
|
||||
belowItem,
|
||||
})
|
||||
? "compact"
|
||||
: "default";
|
||||
return (
|
||||
<AssistantMessage
|
||||
message={item.text}
|
||||
@@ -344,8 +388,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
workspaceRoot={workspaceRoot}
|
||||
serverId={serverId}
|
||||
client={client}
|
||||
spacing={spacing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "thought": {
|
||||
const nextItem = getStreamNeighborItem({
|
||||
strategy: streamRenderStrategy,
|
||||
@@ -485,21 +531,27 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
);
|
||||
|
||||
const showWorkingIndicator = agent.status === "running";
|
||||
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||
const pendingPermissionsNode =
|
||||
const pendingPermissionsNode = useMemo(
|
||||
() =>
|
||||
pendingPermissionItems.length > 0 ? (
|
||||
<View style={stylesheet.permissionsContainer}>
|
||||
{pendingPermissionItems.map((permission) => (
|
||||
<PermissionRequestCard key={permission.key} permission={permission} client={client} />
|
||||
))}
|
||||
</View>
|
||||
) : null;
|
||||
const workingIndicatorNode = showWorkingIndicator ? (
|
||||
<View style={stylesheet.bottomBarWrapper}>
|
||||
<WorkingIndicator />
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
) : null,
|
||||
[client, pendingPermissionItems],
|
||||
);
|
||||
const workingIndicatorNode = useMemo(
|
||||
() =>
|
||||
showWorkingIndicator ? (
|
||||
<View style={stylesheet.bottomBarWrapper}>
|
||||
<WorkingIndicator />
|
||||
</View>
|
||||
) : null,
|
||||
[showWorkingIndicator],
|
||||
);
|
||||
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||
return {
|
||||
...baseRenderModel,
|
||||
boundary: {
|
||||
@@ -514,7 +566,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
workingIndicator: workingIndicatorNode,
|
||||
},
|
||||
};
|
||||
}, [baseRenderModel, client, getGapBetween, pendingPermissionItems, showWorkingIndicator]);
|
||||
}, [baseRenderModel, getGapBetween, pendingPermissionsNode, workingIndicatorNode]);
|
||||
|
||||
const listEmptyComponent = useMemo(() => {
|
||||
if (
|
||||
|
||||
@@ -433,6 +433,7 @@ interface AssistantMessageProps {
|
||||
serverId?: string;
|
||||
client?: DaemonClient | null;
|
||||
disableOuterSpacing?: boolean;
|
||||
spacing?: "default" | "compact";
|
||||
}
|
||||
|
||||
export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
@@ -440,6 +441,10 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[3],
|
||||
},
|
||||
containerCompactVertical: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
containerSpacing: {
|
||||
marginBottom: theme.spacing[4],
|
||||
},
|
||||
@@ -952,6 +957,115 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
interface NativeExpandableBadgeShimmerProps {
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
rowWidth: number;
|
||||
rowHeight: number;
|
||||
peakWidth: number;
|
||||
durationSeconds: number;
|
||||
gradientId: string;
|
||||
}
|
||||
|
||||
const NativeExpandableBadgeShimmer = memo(function NativeExpandableBadgeShimmer({
|
||||
label,
|
||||
secondaryLabel,
|
||||
rowWidth,
|
||||
rowHeight,
|
||||
peakWidth,
|
||||
durationSeconds,
|
||||
gradientId,
|
||||
}: NativeExpandableBadgeShimmerProps) {
|
||||
const shimmerTranslateX = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
const startPosition = -peakWidth;
|
||||
const endPosition = rowWidth + peakWidth;
|
||||
shimmerTranslateX.value = startPosition;
|
||||
shimmerTranslateX.value = withRepeat(
|
||||
withTiming(endPosition, {
|
||||
duration: durationSeconds * 1000,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
-1,
|
||||
false,
|
||||
);
|
||||
return () => {
|
||||
cancelAnimation(shimmerTranslateX);
|
||||
};
|
||||
}, [durationSeconds, peakWidth, rowWidth, shimmerTranslateX]);
|
||||
|
||||
const nativeShimmerPeakStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: shimmerTranslateX.value }],
|
||||
}));
|
||||
|
||||
const nativeShimmerTrackStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.nativeShimmerTrack, { width: rowWidth, height: rowHeight }],
|
||||
[rowHeight, rowWidth],
|
||||
);
|
||||
|
||||
const nativeShimmerMaskStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.shimmerMaskRow, { width: rowWidth, height: rowHeight }],
|
||||
[rowHeight, rowWidth],
|
||||
);
|
||||
|
||||
const nativeLabelMaskStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.label, { color: "#000000", opacity: 1 }],
|
||||
[],
|
||||
);
|
||||
|
||||
const nativeSecondaryMaskStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.secondaryLabel, { color: "#000000", opacity: 1 }],
|
||||
[],
|
||||
);
|
||||
|
||||
const nativeShimmerPeakCombinedStyle = useMemo(
|
||||
() => [
|
||||
expandableBadgeStylesheet.nativeShimmerPeak,
|
||||
nativeShimmerPeakStyle,
|
||||
{ width: peakWidth, height: rowHeight },
|
||||
],
|
||||
[nativeShimmerPeakStyle, peakWidth, rowHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={expandableBadgeStylesheet.shimmerOverlay} pointerEvents="none">
|
||||
<MaskedView
|
||||
style={nativeShimmerTrackStyle}
|
||||
maskElement={
|
||||
<View style={nativeShimmerMaskStyle}>
|
||||
<Text style={nativeLabelMaskStyle} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{secondaryLabel ? (
|
||||
<Text style={nativeSecondaryMaskStyle} numberOfLines={1}>
|
||||
{secondaryLabel}
|
||||
</Text>
|
||||
) : (
|
||||
<View style={expandableBadgeStylesheet.spacer} />
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<View style={nativeShimmerTrackStyle}>
|
||||
<Animated.View style={nativeShimmerPeakCombinedStyle}>
|
||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||
<Defs>
|
||||
<SvgLinearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<Stop offset="0%" stopColor="#ffffff" stopOpacity={0} />
|
||||
<Stop offset="50%" stopColor="#ffffff" stopOpacity={1} />
|
||||
<Stop offset="100%" stopColor="#ffffff" stopOpacity={0} />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
<Rect x="0" y="0" width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</MaskedView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
interface MemoizedMarkdownBlockProps {
|
||||
text: string;
|
||||
styles: ReturnType<typeof createMarkdownStyles>;
|
||||
@@ -989,9 +1103,12 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
serverId,
|
||||
client,
|
||||
disableOuterSpacing,
|
||||
spacing = "default",
|
||||
}: AssistantMessageProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
||||
const resolvedDisableOuterSpacing = useDisableOuterSpacing(
|
||||
disableOuterSpacing ?? spacing === "compact",
|
||||
);
|
||||
|
||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||
|
||||
@@ -1192,6 +1309,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
testID="assistant-message"
|
||||
style={[
|
||||
assistantMessageStylesheet.container,
|
||||
spacing === "compact" && assistantMessageStylesheet.containerCompactVertical,
|
||||
!resolvedDisableOuterSpacing && assistantMessageStylesheet.containerSpacing,
|
||||
]}
|
||||
>
|
||||
@@ -1670,7 +1788,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
const [labelWidth, setLabelWidth] = useState(0);
|
||||
const [secondaryOffsetX, setSecondaryOffsetX] = useState(0);
|
||||
const [secondaryWidth, setSecondaryWidth] = useState(0);
|
||||
const shimmerTranslateX = useSharedValue(0);
|
||||
|
||||
const totalShimmerChars = label.trim().length + (secondaryLabel?.trim().length ?? 0);
|
||||
const shortTextDurationAdjustment = totalShimmerChars <= 12 ? 0.25 : 0;
|
||||
@@ -1738,28 +1855,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
ensureWebToolCallShimmerKeyframes();
|
||||
}, [isWebShimmer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNativeShimmer) {
|
||||
cancelAnimation(shimmerTranslateX);
|
||||
shimmerTranslateX.value = -nativeShimmerPeakWidth;
|
||||
return;
|
||||
}
|
||||
const startPosition = -nativeShimmerPeakWidth;
|
||||
const endPosition = labelRowWidth + nativeShimmerPeakWidth;
|
||||
shimmerTranslateX.value = startPosition;
|
||||
shimmerTranslateX.value = withRepeat(
|
||||
withTiming(endPosition, {
|
||||
duration: shimmerDuration * 1000,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
-1,
|
||||
false,
|
||||
);
|
||||
return () => {
|
||||
cancelAnimation(shimmerTranslateX);
|
||||
};
|
||||
}, [isNativeShimmer, labelRowWidth, nativeShimmerPeakWidth, shimmerDuration, shimmerTranslateX]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNative || !isExpanded || !hasDetailContent) {
|
||||
return;
|
||||
@@ -1784,10 +1879,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
};
|
||||
}, [hasDetailContent, isExpanded, wheelInvestigationComponentId]);
|
||||
|
||||
const nativeShimmerPeakStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: shimmerTranslateX.value }],
|
||||
}));
|
||||
|
||||
const shimmerGradient =
|
||||
"linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.45) 24%, #ffffff 40%, #ffffff 60%, rgba(255, 255, 255, 0.45) 76%, rgba(255, 255, 255, 0) 100%)";
|
||||
|
||||
@@ -1887,41 +1978,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
[shimmerSecondaryStyle],
|
||||
);
|
||||
|
||||
const nativeShimmerTrackStyle = useMemo(
|
||||
() => [
|
||||
expandableBadgeStylesheet.nativeShimmerTrack,
|
||||
{ width: labelRowWidth, height: labelRowHeight },
|
||||
],
|
||||
[labelRowHeight, labelRowWidth],
|
||||
);
|
||||
|
||||
const nativeShimmerMaskStyle = useMemo(
|
||||
() => [
|
||||
expandableBadgeStylesheet.shimmerMaskRow,
|
||||
{ width: labelRowWidth, height: labelRowHeight },
|
||||
],
|
||||
[labelRowHeight, labelRowWidth],
|
||||
);
|
||||
|
||||
const nativeLabelMaskStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.label, { color: "#000000", opacity: 1 }],
|
||||
[],
|
||||
);
|
||||
|
||||
const nativeSecondaryMaskStyle = useMemo(
|
||||
() => [expandableBadgeStylesheet.secondaryLabel, { color: "#000000", opacity: 1 }],
|
||||
[],
|
||||
);
|
||||
|
||||
const nativeShimmerPeakCombinedStyle = useMemo(
|
||||
() => [
|
||||
expandableBadgeStylesheet.nativeShimmerPeak,
|
||||
nativeShimmerPeakStyle,
|
||||
{ width: nativeShimmerPeakWidth, height: labelRowHeight },
|
||||
],
|
||||
[labelRowHeight, nativeShimmerPeakStyle, nativeShimmerPeakWidth],
|
||||
);
|
||||
|
||||
const chevronStyle = useMemo(
|
||||
() => [
|
||||
expandableBadgeStylesheet.chevron,
|
||||
@@ -2003,52 +2059,15 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
</View>
|
||||
) : null}
|
||||
{isNativeShimmer ? (
|
||||
<View style={expandableBadgeStylesheet.shimmerOverlay} pointerEvents="none">
|
||||
<MaskedView
|
||||
style={nativeShimmerTrackStyle}
|
||||
maskElement={
|
||||
<View style={nativeShimmerMaskStyle}>
|
||||
<Text style={nativeLabelMaskStyle} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{secondaryLabel ? (
|
||||
<Text style={nativeSecondaryMaskStyle} numberOfLines={1}>
|
||||
{secondaryLabel}
|
||||
</Text>
|
||||
) : (
|
||||
<View style={expandableBadgeStylesheet.spacer} />
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<View style={nativeShimmerTrackStyle}>
|
||||
<Animated.View style={nativeShimmerPeakCombinedStyle}>
|
||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||
<Defs>
|
||||
<SvgLinearGradient
|
||||
id={nativeGradientIdRef.current}
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="0%"
|
||||
>
|
||||
<Stop offset="0%" stopColor="#ffffff" stopOpacity={0} />
|
||||
<Stop offset="50%" stopColor="#ffffff" stopOpacity={1} />
|
||||
<Stop offset="100%" stopColor="#ffffff" stopOpacity={0} />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
<Rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill={`url(#${nativeGradientIdRef.current})`}
|
||||
/>
|
||||
</Svg>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</MaskedView>
|
||||
</View>
|
||||
<NativeExpandableBadgeShimmer
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabel}
|
||||
rowWidth={labelRowWidth}
|
||||
rowHeight={labelRowHeight}
|
||||
peakWidth={nativeShimmerPeakWidth}
|
||||
durationSeconds={shimmerDuration}
|
||||
gradientId={nativeGradientIdRef.current}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{isInteractive && isHovered ? (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type NativeSyntheticEvent,
|
||||
} from "react-native";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { useBottomAnchorController } from "./use-bottom-anchor-controller";
|
||||
import type { StreamRenderInput, StreamStrategy, StreamViewportHandle } from "./stream-strategy";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
||||
baseListContentContainerStyle,
|
||||
strategy,
|
||||
} = props;
|
||||
const { renderHistoryMountedRow, renderLiveHeadRow, renderLiveAuxiliary } = renderers;
|
||||
const flatListRef = useRef<FlatList<StreamItem>>(null);
|
||||
const streamViewportMetricsRef = useRef({
|
||||
containerKey: "native-virtualized",
|
||||
@@ -190,105 +192,91 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
||||
};
|
||||
}, [agentId, bottomAnchorController, markNativeViewportSettling, viewportRef]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
||||
const previousOffsetY = scrollOffsetYRef.current;
|
||||
scrollOffsetYRef.current = contentOffset.y;
|
||||
streamViewportMetricsRef.current = {
|
||||
contentHeight: Math.max(0, contentSize.height),
|
||||
viewportWidth: Math.max(0, layoutMeasurement.width),
|
||||
viewportHeight: Math.max(0, layoutMeasurement.height),
|
||||
containerKey: "native-virtualized",
|
||||
offsetY: contentOffset.y,
|
||||
viewportMeasuredForKey: "native-virtualized",
|
||||
contentMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
const handleScroll = useStableEvent((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
||||
const previousOffsetY = scrollOffsetYRef.current;
|
||||
scrollOffsetYRef.current = contentOffset.y;
|
||||
streamViewportMetricsRef.current = {
|
||||
contentHeight: Math.max(0, contentSize.height),
|
||||
viewportWidth: Math.max(0, layoutMeasurement.width),
|
||||
viewportHeight: Math.max(0, layoutMeasurement.height),
|
||||
containerKey: "native-virtualized",
|
||||
offsetY: contentOffset.y,
|
||||
viewportMeasuredForKey: "native-virtualized",
|
||||
contentMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
|
||||
const nearBottom = isNearBottomForStreamRenderStrategy({
|
||||
strategy,
|
||||
offsetY: contentOffset.y,
|
||||
threshold: 32,
|
||||
contentHeight: streamViewportMetricsRef.current.contentHeight,
|
||||
viewportHeight: streamViewportMetricsRef.current.viewportHeight,
|
||||
const nearBottom = isNearBottomForStreamRenderStrategy({
|
||||
strategy,
|
||||
offsetY: contentOffset.y,
|
||||
threshold: 32,
|
||||
contentHeight: streamViewportMetricsRef.current.contentHeight,
|
||||
viewportHeight: streamViewportMetricsRef.current.viewportHeight,
|
||||
});
|
||||
onNearBottomChange(nearBottom);
|
||||
|
||||
if (programmaticScrollEventBudgetRef.current > 0 && contentOffset.y <= 8) {
|
||||
programmaticScrollEventBudgetRef.current -= 1;
|
||||
} else {
|
||||
programmaticScrollEventBudgetRef.current = 0;
|
||||
bottomAnchorController.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: nearBottom,
|
||||
scrollDelta: contentOffset.y - previousOffsetY,
|
||||
});
|
||||
onNearBottomChange(nearBottom);
|
||||
}
|
||||
});
|
||||
|
||||
if (programmaticScrollEventBudgetRef.current > 0 && contentOffset.y <= 8) {
|
||||
programmaticScrollEventBudgetRef.current -= 1;
|
||||
} else {
|
||||
programmaticScrollEventBudgetRef.current = 0;
|
||||
bottomAnchorController.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: nearBottom,
|
||||
scrollDelta: contentOffset.y - previousOffsetY,
|
||||
});
|
||||
}
|
||||
},
|
||||
[bottomAnchorController, onNearBottomChange, strategy],
|
||||
);
|
||||
const handleListLayout = useStableEvent((event: LayoutChangeEvent) => {
|
||||
const previousViewportWidth = streamViewportMetricsRef.current.viewportWidth;
|
||||
const previousViewportHeight = streamViewportMetricsRef.current.viewportHeight;
|
||||
const viewportWidth = Math.max(0, event.nativeEvent.layout.width);
|
||||
const viewportHeight = Math.max(0, event.nativeEvent.layout.height);
|
||||
const viewportChanged =
|
||||
(previousViewportWidth > 0 && previousViewportWidth !== viewportWidth) ||
|
||||
(previousViewportHeight > 0 && previousViewportHeight !== viewportHeight);
|
||||
streamViewportMetricsRef.current = {
|
||||
...streamViewportMetricsRef.current,
|
||||
containerKey: "native-virtualized",
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
viewportMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
if (viewportChanged) {
|
||||
markNativeViewportSettling();
|
||||
}
|
||||
bottomAnchorController.handleViewportMetricsChange({
|
||||
previousViewportWidth,
|
||||
viewportWidth,
|
||||
previousViewportHeight,
|
||||
viewportHeight,
|
||||
});
|
||||
});
|
||||
|
||||
const handleListLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => {
|
||||
const previousViewportWidth = streamViewportMetricsRef.current.viewportWidth;
|
||||
const previousViewportHeight = streamViewportMetricsRef.current.viewportHeight;
|
||||
const viewportWidth = Math.max(0, event.nativeEvent.layout.width);
|
||||
const viewportHeight = Math.max(0, event.nativeEvent.layout.height);
|
||||
const viewportChanged =
|
||||
(previousViewportWidth > 0 && previousViewportWidth !== viewportWidth) ||
|
||||
(previousViewportHeight > 0 && previousViewportHeight !== viewportHeight);
|
||||
streamViewportMetricsRef.current = {
|
||||
...streamViewportMetricsRef.current,
|
||||
containerKey: "native-virtualized",
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
viewportMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
if (viewportChanged) {
|
||||
markNativeViewportSettling();
|
||||
}
|
||||
bottomAnchorController.handleViewportMetricsChange({
|
||||
previousViewportWidth,
|
||||
viewportWidth,
|
||||
previousViewportHeight,
|
||||
viewportHeight,
|
||||
});
|
||||
},
|
||||
[bottomAnchorController, markNativeViewportSettling],
|
||||
);
|
||||
const handleContentSizeChange = useStableEvent((_width: number, height: number) => {
|
||||
const previousContentHeight = streamViewportMetricsRef.current.contentHeight;
|
||||
const nextContentHeight = Math.max(0, height);
|
||||
streamViewportMetricsRef.current = {
|
||||
...streamViewportMetricsRef.current,
|
||||
containerKey: "native-virtualized",
|
||||
contentHeight: nextContentHeight,
|
||||
contentMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
bottomAnchorController.handleContentSizeChange({
|
||||
previousContentHeight,
|
||||
contentHeight: nextContentHeight,
|
||||
});
|
||||
});
|
||||
|
||||
const handleContentSizeChange = useCallback(
|
||||
(_width: number, height: number) => {
|
||||
const previousContentHeight = streamViewportMetricsRef.current.contentHeight;
|
||||
const nextContentHeight = Math.max(0, height);
|
||||
streamViewportMetricsRef.current = {
|
||||
...streamViewportMetricsRef.current,
|
||||
containerKey: "native-virtualized",
|
||||
contentHeight: nextContentHeight,
|
||||
contentMeasuredForKey: "native-virtualized",
|
||||
};
|
||||
bottomAnchorController.handleContentSizeChange({
|
||||
previousContentHeight,
|
||||
contentHeight: nextContentHeight,
|
||||
});
|
||||
},
|
||||
[bottomAnchorController],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: ListRenderItemInfo<StreamItem>) => {
|
||||
const rendered = renderers.renderHistoryMountedRow(item, index, historyRows);
|
||||
return rendered ? <Fragment>{rendered}</Fragment> : null;
|
||||
},
|
||||
[historyRows, renderers],
|
||||
);
|
||||
const renderItem = useStableEvent(({ item, index }: ListRenderItemInfo<StreamItem>) => {
|
||||
const rendered = renderHistoryMountedRow(item, index, historyRows);
|
||||
return rendered ? <Fragment>{rendered}</Fragment> : null;
|
||||
});
|
||||
|
||||
const liveHeaderContent = useMemo(() => {
|
||||
const liveHeadRows = segments.liveHead.map((item, index) => (
|
||||
<Fragment key={item.id}>
|
||||
{renderers.renderLiveHeadRow(item, index, segments.liveHead)}
|
||||
</Fragment>
|
||||
<Fragment key={item.id}>{renderLiveHeadRow(item, index, segments.liveHead)}</Fragment>
|
||||
));
|
||||
const liveAuxiliary = renderers.renderLiveAuxiliary();
|
||||
const liveAuxiliary = renderLiveAuxiliary();
|
||||
if (
|
||||
liveHeadRows.length === 0 &&
|
||||
!liveAuxiliary &&
|
||||
@@ -303,7 +291,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
||||
{liveAuxiliary}
|
||||
</Fragment>
|
||||
);
|
||||
}, [boundary, listEmptyComponent, renderers, segments.liveHead]);
|
||||
}, [boundary, listEmptyComponent, renderLiveAuxiliary, renderLiveHeadRow, segments.liveHead]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
@@ -321,8 +309,11 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
||||
scrollEventThrottle={16}
|
||||
onContentSizeChange={handleContentSizeChange}
|
||||
maintainVisibleContentPosition={DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION}
|
||||
initialNumToRender={12}
|
||||
windowSize={10}
|
||||
initialNumToRender={40}
|
||||
maxToRenderPerBatch={40}
|
||||
updateCellsBatchingPeriod={0}
|
||||
windowSize={21}
|
||||
removeClippedSubviews={false}
|
||||
scrollEnabled={scrollEnabled}
|
||||
showsVerticalScrollIndicator
|
||||
inverted
|
||||
|
||||
Reference in New Issue
Block a user