From d1a226b36384eb69b6dc9a4cfd77d508a3ba83fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20Yumo?= Date: Thu, 16 Jan 2025 19:11:14 +0800 Subject: [PATCH 1/4] refactor: memo mergedContent --- components/bubble/Bubble.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 014c81c2..59d7e7ea 100644 --- a/components/bubble/Bubble.tsx +++ b/components/bubble/Bubble.tsx @@ -109,7 +109,10 @@ const Bubble: React.ForwardRefRenderFunction = (props, r const avatarNode = React.isValidElement(avatar) ? avatar : ; // =========================== Content ============================ - const mergedContent = messageRender ? messageRender(typedContent as any) : typedContent; + const mergedContent = React.useMemo( + () => (messageRender ? messageRender(typedContent as any) : typedContent), + [typedContent, messageRender], + ); // ============================ Render ============================ let contentNode: React.ReactNode; From 3f4e2373f483956dac377e1c1e48f7b70b980372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20Yumo?= Date: Fri, 17 Jan 2025 11:00:32 +0800 Subject: [PATCH 2/4] refactor: memo avatarNode --- components/bubble/Bubble.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 59d7e7ea..10adf1b4 100644 --- a/components/bubble/Bubble.tsx +++ b/components/bubble/Bubble.tsx @@ -106,7 +106,10 @@ const Bubble: React.ForwardRefRenderFunction = (props, r ); // ============================ Avatar ============================ - const avatarNode = React.isValidElement(avatar) ? avatar : ; + const avatarNode = React.useMemo( + () => (React.isValidElement(avatar) ? avatar : ), + [avatar], + ); // =========================== Content ============================ const mergedContent = React.useMemo( From bde6134f24d589e4bf65d6c3e2f1e0b35ca28b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20Yumo?= Date: Sun, 26 Jan 2025 16:38:19 +0800 Subject: [PATCH 3/4] refactor: remove useDisplayData --- components/bubble/BubbleList.tsx | 17 +++------ components/bubble/hooks/useDisplayData.ts | 42 ----------------------- 2 files changed, 5 insertions(+), 54 deletions(-) delete mode 100644 components/bubble/hooks/useDisplayData.ts diff --git a/components/bubble/BubbleList.tsx b/components/bubble/BubbleList.tsx index 52e76226..5c24d7ef 100644 --- a/components/bubble/BubbleList.tsx +++ b/components/bubble/BubbleList.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { useXProviderContext } from '../x-provider'; import Bubble, { BubbleContext } from './Bubble'; import type { BubbleRef } from './Bubble'; -import useDisplayData from './hooks/useDisplayData'; import useListData from './hooks/useListData'; import type { BubbleProps } from './interface'; import useStyle from './style'; @@ -80,8 +79,6 @@ const BubbleList: React.ForwardRefRenderFunction // ============================= Data ============================= const mergedData = useListData(items, roles); - const [displayData, onTypingComplete] = useDisplayData(mergedData); - // ============================ Scroll ============================ // Is current scrollTop at the end. User scroll will make this false. const [scrollReachEnd, setScrollReachEnd] = React.useState(true); @@ -108,7 +105,7 @@ const BubbleList: React.ForwardRefRenderFunction React.useEffect(() => { if (autoScroll) { // New date come, the origin last one is the second last one - const lastItemKey = displayData[displayData.length - 2]?.key; + const lastItemKey = mergedData[mergedData.length - 2]?.key; const bubbleInst = bubbleRefs.current[lastItemKey!]; // Auto scroll if last 2 item is visible @@ -124,7 +121,7 @@ const BubbleList: React.ForwardRefRenderFunction } } } - }, [displayData.length]); + }, [mergedData.length]); // ========================== Outer Ref =========================== React.useImperativeHandle(ref, () => ({ @@ -142,8 +139,8 @@ const BubbleList: React.ForwardRefRenderFunction if (bubbleInst) { // Block current auto scrolling - const index = displayData.findIndex((dataItem) => dataItem.key === key); - setScrollReachEnd(index === displayData.length - 1); + const index = mergedData.findIndex((dataItem) => dataItem.key === key); + setScrollReachEnd(index === mergedData.length - 1); // Do native scroll bubbleInst.nativeElement.scrollIntoView({ @@ -181,7 +178,7 @@ const BubbleList: React.ForwardRefRenderFunction ref={listRef} onScroll={onInternalScroll} > - {displayData.map(({ key, ...bubble }) => ( + {mergedData.map(({ key, ...bubble }) => ( } }} typing={initialized ? bubble.typing : false} - onTypingComplete={() => { - bubble.onTypingComplete?.(); - onTypingComplete(key); - }} /> ))} diff --git a/components/bubble/hooks/useDisplayData.ts b/components/bubble/hooks/useDisplayData.ts deleted file mode 100644 index b7dbbb72..00000000 --- a/components/bubble/hooks/useDisplayData.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useEvent } from 'rc-util'; -import React from 'react'; -import { ListItemType } from './useListData'; - -export default function useDisplayData(items: ListItemType[]) { - const [displayCount, setDisplayCount] = React.useState(items.length); - - const displayList = React.useMemo(() => items.slice(0, displayCount), [items, displayCount]); - - const displayListLastKey = React.useMemo(() => { - const lastItem = displayList[displayList.length - 1]; - return lastItem ? lastItem.key : null; - }, [displayList]); - - // When `items` changed, we replaced with latest one - React.useEffect(() => { - if (displayList.length && displayList.every((item, index) => item.key === items[index]?.key)) { - return; - } - - if (displayList.length === 0) { - setDisplayCount(1); - } else { - // Find diff index - for (let i = 0; i < displayList.length; i += 1) { - if (displayList[i].key !== items[i]?.key) { - setDisplayCount(i); - break; - } - } - } - }, [items]); - - // Continue to show if last one finished typing - const onTypingComplete = useEvent((key: string | number) => { - if (key === displayListLastKey) { - setDisplayCount(displayCount + 1); - } - }); - - return [displayList, onTypingComplete] as const; -} From 069de640584ae502dc08d6eea801b52323dc7496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20Yumo?= Date: Mon, 27 Jan 2025 17:26:11 +0800 Subject: [PATCH 4/4] refactor: memo bubble list item --- components/bubble/BubbleList.tsx | 28 +++++-- components/bubble/demo/debug-list.md | 7 ++ components/bubble/demo/debug-list.tsx | 107 ++++++++++++++++++++++++++ components/bubble/index.en-US.md | 1 + components/bubble/index.zh-CN.md | 1 + 5 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 components/bubble/demo/debug-list.md create mode 100644 components/bubble/demo/debug-list.tsx diff --git a/components/bubble/BubbleList.tsx b/components/bubble/BubbleList.tsx index 5c24d7ef..97192549 100644 --- a/components/bubble/BubbleList.tsx +++ b/components/bubble/BubbleList.tsx @@ -36,6 +36,24 @@ export interface BubbleListProps extends React.HTMLAttributes { roles?: RolesType; } +const BubbleListItem: React.ForwardRefRenderFunction, BubbleProps> = ( + props, + ref, +) => ( + { + if (node) { + (ref as React.RefObject>).current[props.id!] = node; + } else { + delete (ref as React.RefObject>).current?.[props.id!]; + } + }} + /> +); + +const MemoBubbleListItem = React.memo(React.forwardRef(BubbleListItem)); + const TOLERANCE = 1; const BubbleList: React.ForwardRefRenderFunction = (props, ref) => { @@ -179,16 +197,10 @@ const BubbleList: React.ForwardRefRenderFunction onScroll={onInternalScroll} > {mergedData.map(({ key, ...bubble }) => ( - { - if (node) { - bubbleRefs.current[key] = node; - } else { - delete bubbleRefs.current[key]; - } - }} + ref={bubbleRefs} typing={initialized ? bubble.typing : false} /> ))} diff --git a/components/bubble/demo/debug-list.md b/components/bubble/demo/debug-list.md new file mode 100644 index 00000000..baf8707e --- /dev/null +++ b/components/bubble/demo/debug-list.md @@ -0,0 +1,7 @@ +## zh-CN + +仅用于调制多种场景 Bubble.List 是否符合预期 + +## en-US + +Only use in debug to check Bubble.List diff --git a/components/bubble/demo/debug-list.tsx b/components/bubble/demo/debug-list.tsx new file mode 100644 index 00000000..70b44df1 --- /dev/null +++ b/components/bubble/demo/debug-list.tsx @@ -0,0 +1,107 @@ +import { Bubble } from '@ant-design/x'; +import { App, Button, Flex, type GetProps, type GetRef, Select } from 'antd'; +import React from 'react'; + +type BubbleListItems = Required>['items']; + +const Demo = () => { + const { message } = App.useApp(); + + const [bubbleList, setBubbleList] = React.useState([ + { + id: '0', + key: '0', + content: `#0 - message content`, + typing: true, + }, + ]); + + const listRef = React.useRef>(null); + + // add a new bubble to the beginning of the list + function unshiftBubble() { + const firstId = bubbleList[0]?.id; + + const id = `${firstId !== undefined ? Number(firstId) - 1 : 0}`; + + setBubbleList((preList) => [ + { + id, + key: id, + content: `#${id} - message content`, + }, + ...preList, + ]); + + message.success(`#${id} message rendered!`); + } + + // add a new bubble to the end of the list + function pushBubble() { + const lastId = bubbleList[bubbleList.length - 1]?.id; + + const id = `${lastId !== undefined ? Number(lastId) + 1 : 0}`; + + setBubbleList((preList) => [ + ...preList, + { + id, + key: id, + content: `#${id} - message content`, + typing: true, + }, + ]); + + message.success(`#${id} message rendered!`); + } + + // remove a bubble from the list + function deleteBubble(id: string) { + setBubbleList((preList) => preList.filter((item) => item.id !== id)); + + message.success(`#${id} message deleted!`); + } + + function scrollTo(id: number) { + listRef.current?.scrollTo?.({ key: id, block: 'nearest' }); + + message.success(`scroll to #${id} message!`); + } + + const selectOptions = React.useMemo( + () => + bubbleList.map((item) => ({ + value: item.id, + label: item.content, + })), + [bubbleList], + ); + + return ( + + + + + + + + + ); +}; + +export default () => ( + + + +); diff --git a/components/bubble/index.en-US.md b/components/bubble/index.en-US.md index dd5a8478..aee26966 100644 --- a/components/bubble/index.en-US.md +++ b/components/bubble/index.en-US.md @@ -19,6 +19,7 @@ Often used when chatting. debug +debug list Basic Placement and avatar Header and footer diff --git a/components/bubble/index.zh-CN.md b/components/bubble/index.zh-CN.md index 66e122fa..4f176916 100644 --- a/components/bubble/index.zh-CN.md +++ b/components/bubble/index.zh-CN.md @@ -20,6 +20,7 @@ demo: debug +debug list 基本 支持位置和头像 头和尾