diff --git a/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx b/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx index 3a525ff9..d9394fcf 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useMemoizedFn, useSharedEventHandler } from 'tailchat-shared'; import { ChatMessageHeader } from './ChatMessageHeader'; import { buildMessageItemRow } from './Item'; +import { ScrollToBottom } from './ScrollToBottom'; import type { MessageListProps } from './types'; /** @@ -9,6 +10,7 @@ import type { MessageListProps } from './types'; * 并处理在某些场景下计算位置会少1px导致无法正确触发加载的问题 */ const topTriggerBuffer = 100; +const bottomTriggerBuffer = 40; /** * 没有虚拟化版本的聊天列表 @@ -17,6 +19,7 @@ export const NormalMessageList: React.FC = React.memo( (props) => { const containerRef = useRef(null); const lockRef = useRef(false); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); const scrollToBottom = useMemoizedFn(() => { containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); @@ -44,9 +47,10 @@ export const NormalMessageList: React.FC = React.memo( return; } - if (containerRef.current.scrollTop === 0) { + if (-containerRef.current.scrollTop <= bottomTriggerBuffer) { // 滚动到最底部 lockRef.current = false; + setShowScrollToBottom(false); } else if ( -containerRef.current.scrollTop + containerRef.current.clientHeight >= containerRef.current.scrollHeight - topTriggerBuffer @@ -57,6 +61,7 @@ export const NormalMessageList: React.FC = React.memo( // 滚动在中间 // 锁定位置不自动滚动 lockRef.current = true; + setShowScrollToBottom(true); } }, [props.messages]); @@ -72,6 +77,8 @@ export const NormalMessageList: React.FC = React.memo( )} + {showScrollToBottom && } + {/* 因为是倒过来的,因此要前面的要放在后面 */} {props.title && !props.hasMoreMessage && ( diff --git a/client/web/src/components/ChatBox/ChatMessageList/ScrollToBottom.tsx b/client/web/src/components/ChatBox/ChatMessageList/ScrollToBottom.tsx new file mode 100644 index 00000000..80bab4ed --- /dev/null +++ b/client/web/src/components/ChatBox/ChatMessageList/ScrollToBottom.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Icon } from 'tailchat-design'; + +interface Props { + onClick: () => void; +} + +/** + * 滚动到底部的按钮 + */ +export const ScrollToBottom: React.FC = React.memo((props) => { + return ( +
+ +
+ ); +}); +ScrollToBottom.displayName = 'ScrollToBottom'; diff --git a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx index 62e30336..a2922152 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, { useMemo, useRef } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { buildMessageItemRow } from './Item'; import type { MessageListProps } from './types'; import { @@ -11,6 +11,7 @@ import { useMemoizedFn, useSharedEventHandler, } from 'tailchat-shared'; +import { ScrollToBottom } from './ScrollToBottom'; const PREPEND_OFFSET = 10 ** 7; @@ -18,6 +19,11 @@ const virtuosoStyle: React.CSSProperties = { height: '100%', }; +const overscan = { + main: 1000, + reverse: 1000, +}; + /** * 新版的虚拟列表 * 参考: https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/VirtualizedMessageList.tsx @@ -25,16 +31,18 @@ const virtuosoStyle: React.CSSProperties = { export const VirtualizedMessageList: React.FC = React.memo( (props) => { const listRef = useRef(null); + const scrollerRef = useRef(); const numItemsPrepended = usePrependedMessagesCount(props.messages); - useSharedEventHandler('sendMessage', () => { - listRef.current?.scrollToIndex({ - index: 'LAST', - align: 'end', + const scrollToBottom = useMemoizedFn(() => { + listRef.current?.scrollTo({ + top: scrollerRef.current?.scrollHeight, behavior: 'smooth', }); }); + useSharedEventHandler('sendMessage', scrollToBottom); + const handleLoadMore = useMemoizedFn(() => { if (props.isLoadingMore) { return; @@ -71,27 +79,38 @@ export const VirtualizedMessageList: React.FC = React.memo( return buildMessageItemRow(props.messages, index); }); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); + const atBottomStateChange = useMemoizedFn((atBottom: boolean) => { + if (atBottom) { + setShowScrollToBottom(false); + } else { + setShowScrollToBottom(true); + } + }); + return (
(scrollerRef.current = ref as HTMLElement)} firstItemIndex={PREPEND_OFFSET - numItemsPrepended} initialTopMostItemIndex={Math.max(props.messages.length - 1, 0)} computeItemKey={computeItemKey} totalCount={props.messages.length} - overscan={{ - main: 1000, - reverse: 1000, - }} + overscan={overscan} itemContent={itemContent} alignToBottom={true} startReached={handleLoadMore} + atBottomStateChange={atBottomStateChange} followOutput={followOutput} defaultItemHeight={25} atTopThreshold={100} atBottomThreshold={40} + useWindowScroll={false} /> + + {showScrollToBottom && }
); } diff --git a/client/web/src/components/ChatBox/index.tsx b/client/web/src/components/ChatBox/index.tsx index 1ba48da3..b4f75e7d 100644 --- a/client/web/src/components/ChatBox/index.tsx +++ b/client/web/src/components/ChatBox/index.tsx @@ -46,7 +46,7 @@ const ChatBoxInner: React.FC = React.memo((props) => { } return ( -
+