feat(chat): upgrade @legendapp/list to 3.3.8 and let it own end following
3.3.x makes maintainScrollAtEnd follow content growth on its own — a tail row growing in place included — which is exactly what the manual totalSize correction existed for. Delete that correction (the totalSize listener now only drives the anchored-turn glide) and pick up 3.3.x's measurement batching, prepend-flash fixes, and web programmatic-scroll fixes. Opt the explicit maintainScrollAtEnd config into footerLayout per the 3.1.1 guidance. The library's own released-on-user-scroll heuristic proved unreliable one run in three against synthetic touch, so the gesture state machine stays authoritative: while a real gesture owns the scroll, the list's end pinning is switched off through a threaded endPinningReleased prop and re-engages when the user returns to the end. Validated with the CDP battery on a production build: stream follow stays at distance 0, mobile drag releases with the pill shown in three of three runs, resize oscillation stays at the reduced level, the rail reaches the last turn, and profiled streaming cost per rendered character matches the tuned 3.2.0 numbers.
This commit is contained in:
@@ -173,6 +173,7 @@ type ChatViewportProps = {
|
||||
fallbackTimestamp?: number;
|
||||
} | null;
|
||||
scrollToBottom: () => void;
|
||||
endPinningReleased: boolean;
|
||||
sessionQuestions: QuestionRequest[];
|
||||
sessionPermissions: PermissionRequest[];
|
||||
isProgrammaticFollowActive: boolean;
|
||||
@@ -209,6 +210,7 @@ const ChatViewport = React.memo(({
|
||||
activeStreamingPhase,
|
||||
retryOverlay,
|
||||
scrollToBottom,
|
||||
endPinningReleased,
|
||||
sessionQuestions,
|
||||
sessionPermissions,
|
||||
isProgrammaticFollowActive,
|
||||
@@ -392,6 +394,7 @@ const ChatViewport = React.memo(({
|
||||
retryOverlay={retryOverlay}
|
||||
isLoadingOlder={isLoadingOlder}
|
||||
scrollToBottom={scrollToBottom}
|
||||
endPinningReleased={endPinningReleased}
|
||||
directory={directory}
|
||||
registerList={registerList}
|
||||
anchorMessageId={anchorMessageId}
|
||||
@@ -438,6 +441,7 @@ const ChatViewport = React.memo(({
|
||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||
&& prev.retryOverlay === next.retryOverlay
|
||||
&& prev.scrollToBottom === next.scrollToBottom
|
||||
&& prev.endPinningReleased === next.endPinningReleased
|
||||
&& prev.sessionQuestions === next.sessionQuestions
|
||||
&& prev.sessionPermissions === next.sessionPermissions
|
||||
&& prev.isProgrammaticFollowActive === next.isProgrammaticFollowActive
|
||||
@@ -1353,6 +1357,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
retryOverlay={retryOverlay}
|
||||
scrollToBottom={resumeToLatestInstant}
|
||||
endPinningReleased={userOwnsScroll}
|
||||
sessionQuestions={sessionQuestions}
|
||||
sessionPermissions={sessionPermissions}
|
||||
isProgrammaticFollowActive={isFollowingProgrammatically}
|
||||
|
||||
@@ -322,6 +322,9 @@ interface MessageListProps {
|
||||
// The list owns its scroll container; the timeline scroll hook drives it
|
||||
// through this ref and observes it through the callbacks below.
|
||||
registerList?: (list: LegendListRef | null) => void;
|
||||
// True while a real gesture owns the scroll; releases the list's own
|
||||
// end pinning so the state machine, not the library heuristic, decides.
|
||||
endPinningReleased?: boolean;
|
||||
// The anchored row is identified by message id; the index it maps to is a
|
||||
// property of the row model, which only this component knows.
|
||||
anchorMessageId?: string | null;
|
||||
@@ -948,6 +951,7 @@ type TimelineListProps = {
|
||||
entries: RenderEntry[];
|
||||
streamingTailKey: string | null;
|
||||
registerList: (list: LegendListRef | null) => void;
|
||||
endPinningReleased: boolean;
|
||||
anchoredEndSpace?: {
|
||||
anchorIndex: number;
|
||||
anchorOffset?: number;
|
||||
@@ -966,6 +970,7 @@ type TimelineListProps = {
|
||||
const TimelineList = React.memo(({
|
||||
entries,
|
||||
registerList,
|
||||
endPinningReleased,
|
||||
anchoredEndSpace,
|
||||
composerOverlayHeight,
|
||||
onIsAtEndChange,
|
||||
@@ -1065,9 +1070,9 @@ const TimelineList = React.memo(({
|
||||
// rows that are still re-measuring shakes the pinned
|
||||
// viewport; the owning hook re-asserts the end once the
|
||||
// resize settles.
|
||||
maintainScrollAtEnd={anchoredEndSpace || !streamingAutoFollowEnabled || isWidthResizing
|
||||
maintainScrollAtEnd={anchoredEndSpace || !streamingAutoFollowEnabled || isWidthResizing || endPinningReleased
|
||||
? false
|
||||
: { animated: false, on: { dataChange: true, itemLayout: true, layout: true } }}
|
||||
: { animated: false, on: { dataChange: true, itemLayout: true, layout: true, footerLayout: true } }}
|
||||
// Prepending older history must not move what the user is
|
||||
// reading. Size restoration applies only during a width
|
||||
// resize — see the observer above.
|
||||
@@ -1164,6 +1169,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
scrollToBottom,
|
||||
directory,
|
||||
registerList,
|
||||
endPinningReleased = false,
|
||||
anchorMessageId = null,
|
||||
onAnchorReady,
|
||||
onAnchorSizeChanged,
|
||||
@@ -1791,6 +1797,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
listFooter={listFooter}
|
||||
scrollContainerProps={scrollContainerProps}
|
||||
rowContext={rowContext}
|
||||
endPinningReleased={endPinningReleased}
|
||||
/>
|
||||
</FadeInDisabledProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user