Message replies in chat (#10903)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-16 20:26:48 +03:00
committed by GitHub
parent f075187e57
commit b8fdce6478
19 changed files with 925 additions and 180 deletions
+49
View File
@@ -30,6 +30,15 @@ export type ListMethods = {
scrollToTop: () => void
scrollToOffset: (options: {animated: boolean; offset: number}) => void
scrollToEnd: (options?: {animated?: boolean}) => void
// Signature kept compatible with FlatList's scrollToIndex (the native
// ListMethods type) so callers stay platform-agnostic. viewOffset is
// accepted for parity but not currently used by the web implementation.
scrollToIndex: (params: {
animated?: boolean | null
index: number
viewOffset?: number
viewPosition?: number
}) => void
}
export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>,
@@ -216,6 +225,22 @@ function ListImpl<ItemT>(
}, [disableFullWindowScroll])
const nativeRef = useRef<HTMLDivElement>(null)
// Registry of item index -> row DOM node. The list renders header/footer and
// visibility-detector siblings too, so we can't index into the container's
// children directly; each Row registers its own node here keyed by index.
const rowNodesRef = useRef<Map<number, HTMLElement>>(new Map())
const registerRowNode = useCallback(
(index: number, node: HTMLElement | null) => {
if (node) {
rowNodesRef.current.set(index, node)
} else {
rowNodesRef.current.delete(index)
}
},
[],
)
useImperativeHandle(
ref,
() => ({
@@ -239,6 +264,17 @@ function ListImpl<ItemT>(
behavior: animated ? 'smooth' : 'instant',
})
},
scrollToIndex({animated = true, index}) {
const node = rowNodesRef.current.get(index)
// scrollIntoView with block: 'center' roughly matches the caller's
// viewPosition of 0.3 - not exact, but close enough and it respects
// whichever element is the scroll container (window or nativeRef).
node?.scrollIntoView({
block: 'center',
behavior: animated ? 'smooth' : 'instant',
})
},
}),
[getScrollableNode],
)
@@ -392,6 +428,7 @@ function ListImpl<ItemT>(
renderItem={renderItem}
extraData={extraData}
onItemSeen={onItemSeen}
registerRowNode={registerRowNode}
/>
)
})}
@@ -470,6 +507,7 @@ let Row = function RowImpl<ItemT>({
renderItem,
extraData: _unused,
onItemSeen,
registerRowNode,
}: {
item: ItemT
index: number
@@ -479,6 +517,7 @@ let Row = function RowImpl<ItemT>({
| ((info: ListRenderItemInfo<ItemT>) => React.ReactNode)
extraData: unknown
onItemSeen: ((item: ItemT) => void) | undefined
registerRowNode: (index: number, node: HTMLElement | null) => void
}): React.ReactNode {
const rowRef = useRef(null)
const intersectionTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(
@@ -529,6 +568,15 @@ let Row = function RowImpl<ItemT>({
}
}, [handleIntersection, onItemSeen])
// Register this row's DOM node so the list can scroll to it by index.
useEffect(() => {
const node: HTMLElement | null = rowRef.current
registerRowNode(index, node)
return () => {
registerRowNode(index, null)
}
}, [index, registerRowNode])
if (!renderItem) {
return null
}
@@ -552,6 +600,7 @@ Row = memo(Row) as <ItemT>(props: {
| ((info: ListRenderItemInfo<ItemT>) => React.ReactNode)
extraData: unknown
onItemSeen: ((item: ItemT) => void) | undefined
registerRowNode: (index: number, node: HTMLElement | null) => void
}) => React.ReactNode
let Visibility = ({