feat: 增加聊天列表滚动到底部按钮

pull/64/head
moonrailgun 2 years ago
parent eb3b5f9c00
commit 7f1b475f69

@ -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 { useMemoizedFn, useSharedEventHandler } from 'tailchat-shared';
import { ChatMessageHeader } from './ChatMessageHeader'; import { ChatMessageHeader } from './ChatMessageHeader';
import { buildMessageItemRow } from './Item'; import { buildMessageItemRow } from './Item';
import { ScrollToBottom } from './ScrollToBottom';
import type { MessageListProps } from './types'; import type { MessageListProps } from './types';
/** /**
@ -9,6 +10,7 @@ import type { MessageListProps } from './types';
* 1px * 1px
*/ */
const topTriggerBuffer = 100; const topTriggerBuffer = 100;
const bottomTriggerBuffer = 40;
/** /**
* *
@ -17,6 +19,7 @@ export const NormalMessageList: React.FC<MessageListProps> = React.memo(
(props) => { (props) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const lockRef = useRef(false); const lockRef = useRef(false);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const scrollToBottom = useMemoizedFn(() => { const scrollToBottom = useMemoizedFn(() => {
containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
@ -44,9 +47,10 @@ export const NormalMessageList: React.FC<MessageListProps> = React.memo(
return; return;
} }
if (containerRef.current.scrollTop === 0) { if (-containerRef.current.scrollTop <= bottomTriggerBuffer) {
// 滚动到最底部 // 滚动到最底部
lockRef.current = false; lockRef.current = false;
setShowScrollToBottom(false);
} else if ( } else if (
-containerRef.current.scrollTop + containerRef.current.clientHeight >= -containerRef.current.scrollTop + containerRef.current.clientHeight >=
containerRef.current.scrollHeight - topTriggerBuffer containerRef.current.scrollHeight - topTriggerBuffer
@ -57,6 +61,7 @@ export const NormalMessageList: React.FC<MessageListProps> = React.memo(
// 滚动在中间 // 滚动在中间
// 锁定位置不自动滚动 // 锁定位置不自动滚动
lockRef.current = true; lockRef.current = true;
setShowScrollToBottom(true);
} }
}, [props.messages]); }, [props.messages]);
@ -72,6 +77,8 @@ export const NormalMessageList: React.FC<MessageListProps> = React.memo(
)} )}
</div> </div>
{showScrollToBottom && <ScrollToBottom onClick={scrollToBottom} />}
{/* 因为是倒过来的,因此要前面的要放在后面 */} {/* 因为是倒过来的,因此要前面的要放在后面 */}
{props.title && !props.hasMoreMessage && ( {props.title && !props.hasMoreMessage && (
<ChatMessageHeader title={props.title} /> <ChatMessageHeader title={props.title} />

@ -0,0 +1,21 @@
import React from 'react';
import { Icon } from 'tailchat-design';
interface Props {
onClick: () => void;
}
/**
*
*/
export const ScrollToBottom: React.FC<Props> = React.memo((props) => {
return (
<div
className="absolute right-6 bottom-18 px-3 py-2 rounded-full bg-white dark:bg-black bg-opacity-50 shadow cursor-pointer z-10 w-11 h-11 flex justify-center items-center text-2xl hover:bg-opacity-80"
onClick={props.onClick}
>
<Icon icon="mdi:chevron-double-down" />
</div>
);
});
ScrollToBottom.displayName = 'ScrollToBottom';

@ -1,4 +1,4 @@
import React, { useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { buildMessageItemRow } from './Item'; import { buildMessageItemRow } from './Item';
import type { MessageListProps } from './types'; import type { MessageListProps } from './types';
import { import {
@ -11,6 +11,7 @@ import {
useMemoizedFn, useMemoizedFn,
useSharedEventHandler, useSharedEventHandler,
} from 'tailchat-shared'; } from 'tailchat-shared';
import { ScrollToBottom } from './ScrollToBottom';
const PREPEND_OFFSET = 10 ** 7; const PREPEND_OFFSET = 10 ** 7;
@ -18,6 +19,11 @@ const virtuosoStyle: React.CSSProperties = {
height: '100%', height: '100%',
}; };
const overscan = {
main: 1000,
reverse: 1000,
};
/** /**
* *
* : https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/VirtualizedMessageList.tsx * : 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<MessageListProps> = React.memo( export const VirtualizedMessageList: React.FC<MessageListProps> = React.memo(
(props) => { (props) => {
const listRef = useRef<VirtuosoHandle>(null); const listRef = useRef<VirtuosoHandle>(null);
const scrollerRef = useRef<HTMLElement>();
const numItemsPrepended = usePrependedMessagesCount(props.messages); const numItemsPrepended = usePrependedMessagesCount(props.messages);
useSharedEventHandler('sendMessage', () => { const scrollToBottom = useMemoizedFn(() => {
listRef.current?.scrollToIndex({ listRef.current?.scrollTo({
index: 'LAST', top: scrollerRef.current?.scrollHeight,
align: 'end',
behavior: 'smooth', behavior: 'smooth',
}); });
}); });
useSharedEventHandler('sendMessage', scrollToBottom);
const handleLoadMore = useMemoizedFn(() => { const handleLoadMore = useMemoizedFn(() => {
if (props.isLoadingMore) { if (props.isLoadingMore) {
return; return;
@ -71,27 +79,38 @@ export const VirtualizedMessageList: React.FC<MessageListProps> = React.memo(
return buildMessageItemRow(props.messages, index); return buildMessageItemRow(props.messages, index);
}); });
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const atBottomStateChange = useMemoizedFn((atBottom: boolean) => {
if (atBottom) {
setShowScrollToBottom(false);
} else {
setShowScrollToBottom(true);
}
});
return ( return (
<div className="flex-1"> <div className="flex-1">
<Virtuoso <Virtuoso
style={virtuosoStyle} style={virtuosoStyle}
ref={listRef} ref={listRef}
scrollerRef={(ref) => (scrollerRef.current = ref as HTMLElement)}
firstItemIndex={PREPEND_OFFSET - numItemsPrepended} firstItemIndex={PREPEND_OFFSET - numItemsPrepended}
initialTopMostItemIndex={Math.max(props.messages.length - 1, 0)} initialTopMostItemIndex={Math.max(props.messages.length - 1, 0)}
computeItemKey={computeItemKey} computeItemKey={computeItemKey}
totalCount={props.messages.length} totalCount={props.messages.length}
overscan={{ overscan={overscan}
main: 1000,
reverse: 1000,
}}
itemContent={itemContent} itemContent={itemContent}
alignToBottom={true} alignToBottom={true}
startReached={handleLoadMore} startReached={handleLoadMore}
atBottomStateChange={atBottomStateChange}
followOutput={followOutput} followOutput={followOutput}
defaultItemHeight={25} defaultItemHeight={25}
atTopThreshold={100} atTopThreshold={100}
atBottomThreshold={40} atBottomThreshold={40}
useWindowScroll={false}
/> />
{showScrollToBottom && <ScrollToBottom onClick={scrollToBottom} />}
</div> </div>
); );
} }

@ -46,7 +46,7 @@ const ChatBoxInner: React.FC<ChatBoxProps> = React.memo((props) => {
} }
return ( return (
<div className="w-full h-full flex flex-col select-text"> <div className="w-full h-full flex flex-col select-text relative">
<ChatMessageList <ChatMessageList
key={converseId} key={converseId}
title={converseTitle} title={converseTitle}

Loading…
Cancel
Save