diff --git a/client/packages/design/components/VirtualChatList/ResizeWatcher.tsx b/client/packages/design/components/VirtualChatList/ResizeWatcher.tsx index 61273f0c..d7a4db11 100644 --- a/client/packages/design/components/VirtualChatList/ResizeWatcher.tsx +++ b/client/packages/design/components/VirtualChatList/ResizeWatcher.tsx @@ -3,7 +3,7 @@ import { useMemoizedFn } from 'ahooks'; type Size = { height: number; width: number }; -interface ResizeWatcherProps { +interface ResizeWatcherProps extends React.PropsWithChildren { wrapperStyle?: React.CSSProperties; onResize?: (size: Size) => void; } @@ -34,6 +34,7 @@ export const ResizeWatcher: React.FC = React.memo( return; } + // 使用 contentRect 计算大小以确保不会出现使用clientHeight立即向浏览器请求dom大小导致的性能问题 handleResize({ width: Math.round(contentRect.width), height: Math.round(contentRect.height), diff --git a/client/packages/design/components/VirtualChatList/Scroller.tsx b/client/packages/design/components/VirtualChatList/Scroller.tsx index 4c9fe62f..aa16d931 100644 --- a/client/packages/design/components/VirtualChatList/Scroller.tsx +++ b/client/packages/design/components/VirtualChatList/Scroller.tsx @@ -1,12 +1,18 @@ -import React, { PropsWithChildren, useMemo, useRef, useState } from 'react'; +import React, { + PropsWithChildren, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; import { ResizeWatcher } from './ResizeWatcher'; import { useMemoizedFn, useDebounceFn, usePrevious } from 'ahooks'; -import { IsForward, Vector } from './types'; +import type { IsForward, Vector } from './types'; import clsx from 'clsx'; export interface ScrollerRef { scrollTo: (position: Vector) => void; - getMaxPosition: () => Vector; + scrollToBottom: () => void; } type ScrollerProps = PropsWithChildren<{ @@ -15,6 +21,7 @@ type ScrollerProps = PropsWithChildren<{ innerStyle?: React.CSSProperties; isLock?: boolean; scrollingClassName?: string; + scrollBehavior?: ScrollBehavior; onScroll?: ( position: Vector, detail: { @@ -37,27 +44,16 @@ const DEFAULT_POS = { x: 0, y: 0 }; */ export const Scroller = React.forwardRef( (props, ref) => { + const { scrollBehavior = 'auto' } = props; const wrapperRef = useRef(null); const innerRef = useRef(null); const style = useMemo(() => { if (props.isLock ?? false) { return { ...props.style, overflow: 'hidden' }; } - return props.style; - }, []); - const [isScroll, setIsScroll] = useState(false); - const [isMouseDown, setIsMouseDown] = useState(false); - const { run: setIsScrollLazy } = useDebounceFn( - (val) => { - setIsScroll(val); - }, - { - leading: false, - trailing: true, - wait: 300, - } - ); + return { ...props.style, overflow: 'auto' }; + }, [props.isLock]); const getPosition = useMemoizedFn(() => { if (!wrapperRef.current) { @@ -79,6 +75,36 @@ export const Scroller = React.forwardRef( }; }); + useImperativeHandle(ref, () => ({ + scrollTo: (position) => { + wrapperRef.current?.scrollTo({ + left: position.x, + top: position.y, + behavior: scrollBehavior, + }); + }, + scrollToBottom: () => { + wrapperRef.current?.scrollTo({ + left: getPosition().x, + top: wrapperRef.current.scrollHeight - getContainerSize().y, + behavior: scrollBehavior, + }); + }, + })); + + const [isScroll, setIsScroll] = useState(false); + const [isMouseDown, setIsMouseDown] = useState(false); + const { run: setIsScrollLazy } = useDebounceFn( + (val) => { + setIsScroll(val); + }, + { + leading: false, + trailing: true, + wait: 300, + } + ); + const { run: handleEndScrollLazy } = useDebounceFn( () => { setIsScroll(false); @@ -107,7 +133,7 @@ export const Scroller = React.forwardRef( }); const prevPosition = usePrevious(getPosition()) ?? DEFAULT_POS; - const handleMouseScroll = useMemoizedFn(() => { + const handleScroll = useMemoizedFn(() => { const isUserScrolling = isScroll || isMouseDown; const currentPosition = getPosition(); const forward = { @@ -138,7 +164,7 @@ export const Scroller = React.forwardRef(
( onWheel={handleWheel} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} - onScroll={handleMouseScroll} + onScroll={handleScroll} >
diff --git a/client/packages/design/components/VirtualChatList/index.tsx b/client/packages/design/components/VirtualChatList/index.tsx index 19a7e9cb..b15ce457 100644 --- a/client/packages/design/components/VirtualChatList/index.tsx +++ b/client/packages/design/components/VirtualChatList/index.tsx @@ -1,14 +1,82 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { ResizeWatcher } from './ResizeWatcher'; import { Scroller, ScrollerRef } from './Scroller'; +import { useUpdate } from 'ahooks'; -export const VirtualChatList: React.FC = React.memo(() => { +interface VirtualChatListProps { + className?: string; + style?: React.CSSProperties; + innerStyle?: React.CSSProperties; + getItemKey?: (item: ItemType) => string; + items: ItemType[]; + itemContent: (item: ItemType, index: number) => React.ReactNode; +} + +const defaultContainerStyle: React.CSSProperties = { + overflow: 'hidden', +}; + +const defaultInnerStyle: React.CSSProperties = { + height: '100%', +}; + +const scrollerStyle: React.CSSProperties = { + height: '100%', +}; + +const InternalVirtualChatList = ( + props: VirtualChatListProps +) => { const scrollerRef = useRef(null); + const itemHeightCache = useMemo(() => new Map(), []); + const forceUpdate = useUpdate(); + const style = useMemo( + () => ({ + ...defaultContainerStyle, + ...props.style, + }), + [props.style] + ); + const innerStyle = useMemo( + () => ({ + ...defaultInnerStyle, + ...props.innerStyle, + }), + [props.innerStyle] + ); + + useEffect(() => { + // 挂载后滚动到底部 + scrollerRef.current?.scrollToBottom(); + }, []); return ( - - {/* TODO */} -
Foo
-
+
+ + {props.items.map((item, i) => ( +
+ { + itemHeightCache.set(item, size.height); + forceUpdate(); + }} + > + {props.itemContent(item, i)} + +
+ ))} +
+
); -}); +}; + +type VirtualChatListInterface = typeof InternalVirtualChatList & React.FC; + +export const VirtualChatList: VirtualChatListInterface = React.memo( + InternalVirtualChatList +) as any; VirtualChatList.displayName = 'VirtualChatList'; diff --git a/client/packages/design/components/index.ts b/client/packages/design/components/index.ts index fdd45f5b..7293338b 100644 --- a/client/packages/design/components/index.ts +++ b/client/packages/design/components/index.ts @@ -7,6 +7,7 @@ export { Highlight } from './Highlight'; export { Icon } from './Icon'; export { Image } from './Image'; export { SensitiveText } from './SensitiveText'; +export { VirtualChatList } from './VirtualChatList'; export { WebMetaForm } from './WebMetaForm'; export { diff --git a/client/web/package.json b/client/web/package.json index 3625110b..4e795842 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -55,7 +55,7 @@ "react-transition-group": "^4.4.2", "react-use-gesture": "^9.1.3", "react-virtualized-auto-sizer": "^1.0.6", - "react-virtuoso": "^2.18.0", + "react-virtuoso": "^2.19.1", "socket.io-client": "^4.1.2", "source-ref-runtime": "^1.0.7", "styled-components": "^5.3.6", diff --git a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx index ed46f654..62e30336 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { buildMessageItemRow } from './Item'; import type { MessageListProps } from './types'; import { @@ -8,7 +8,6 @@ import { } from 'react-virtuoso'; import { ChatMessage, - sharedEvent, useMemoizedFn, useSharedEventHandler, } from 'tailchat-shared'; @@ -73,25 +72,27 @@ export const VirtualizedMessageList: React.FC = React.memo( }); return ( - +
+ +
); } ); diff --git a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList2.tsx b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList2.tsx new file mode 100644 index 00000000..bacbd539 --- /dev/null +++ b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList2.tsx @@ -0,0 +1,55 @@ +import React, { useCallback } from 'react'; +import { VirtualChatList } from 'tailchat-design'; +import { ChatMessage, useMemoizedFn } from 'tailchat-shared'; +import { buildMessageItemRow } from './Item'; +import type { MessageListProps } from './types'; + +/** + * WIP: + * 自制的虚拟列表 + */ + +const style: React.CSSProperties = { + // height: '100%', + flex: 1, +}; + +export const VirtualizedMessageList: React.FC = React.memo( + (props) => { + // useSharedEventHandler('sendMessage', () => { + // listRef.current?.scrollToIndex({ + // index: 'LAST', + // align: 'end', + // behavior: 'smooth', + // }); + // }); + + // const handleLoadMore = useMemoizedFn(() => { + // if (props.isLoadingMore) { + // return; + // } + + // if (props.hasMoreMessage) { + // props.onLoadMore(); + // } + // }); + + const itemContent = useMemoizedFn((item: ChatMessage, index: number) => { + return buildMessageItemRow(props.messages, index); + }); + + const getItemKey = useCallback((item: ChatMessage) => { + return String(item._id); + }, []); + + return ( + + ); + } +); +VirtualizedMessageList.displayName = 'VirtualizedMessageList'; diff --git a/client/web/src/components/ChatBox/ChatMessageList/index.tsx b/client/web/src/components/ChatBox/ChatMessageList/index.tsx index 08291921..62a506a0 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/index.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/index.tsx @@ -21,9 +21,7 @@ export const ChatMessageList: React.FC = React.memo( } return useVirtualizedList ? ( -
- -
+ ) : ( ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cba6807..25ba867f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,7 +371,7 @@ importers: react-transition-group: ^4.4.2 react-use-gesture: ^9.1.3 react-virtualized-auto-sizer: ^1.0.6 - react-virtuoso: ^2.18.0 + react-virtuoso: ^2.19.1 rimraf: ^3.0.2 rollup-plugin-copy: ^3.4.0 rollup-plugin-replace: ^2.2.0 @@ -434,7 +434,7 @@ importers: react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y react-use-gesture: 9.1.3_react@18.2.0 react-virtualized-auto-sizer: 1.0.6_biqbaboplfbrettd7655fr4n2y - react-virtuoso: 2.18.0_biqbaboplfbrettd7655fr4n2y + react-virtuoso: 2.19.1_biqbaboplfbrettd7655fr4n2y socket.io-client: 4.5.1 source-ref-runtime: 1.0.7 styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba @@ -11474,7 +11474,7 @@ packages: babel-plugin-syntax-jsx: 6.18.0 lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 5.3.6_mdz3marskokvq6744hhidi3r5a + styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba /babel-plugin-syntax-jsx/6.18.0: resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} @@ -26472,6 +26472,7 @@ packages: prop-types: 15.8.1 react: 16.14.0 scheduler: 0.19.1 + dev: false /react-dom/17.0.2_react@17.0.2: resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} @@ -27171,8 +27172,8 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /react-virtuoso/2.18.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-BxMW9as2dPOP4YFkry/oNjQMEn3cOgEjHLb7Fg8oubOgRAfiukp1Co41QFD9ZMXZBNBZNTI2E5BwC5pol31mTg==} + /react-virtuoso/2.19.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-zF6MAwujNGy2nJWCx/Df92ay/RnV2Kj4glUZfdyadI4suAn0kAZHB1BeI7yPFVp2iSccLzFlszhakWyr+fJ4Dw==} engines: {node: '>=10'} peerDependencies: react: '>=16 || >=17 || >= 18' @@ -27191,6 +27192,7 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 prop-types: 15.8.1 + dev: false /react/17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} @@ -28273,6 +28275,7 @@ packages: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 + dev: false /scheduler/0.20.2: resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} @@ -29452,7 +29455,6 @@ packages: react-is: 18.2.0 shallowequal: 1.1.0 supports-color: 5.5.0 - dev: false /styled-components/5.3.6_mdz3marskokvq6744hhidi3r5a: resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==} @@ -29476,6 +29478,7 @@ packages: react-is: 16.13.1 shallowequal: 1.1.0 supports-color: 5.5.0 + dev: false /styled-components/5.3.6_react@18.2.0: resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==}