Improve chat container bottom scrolling handling. Closes #2342

This commit is contained in:
Gabe Kangas 2023-02-18 11:58:46 -08:00
parent 835a644de0
commit 564d058e2e
No known key found for this signature in database
GPG key ID: 4345B2060657F330
2 changed files with 61 additions and 24 deletions

View file

@ -12,7 +12,6 @@ import { ChatMessage } from '../../../interfaces/chat-message.model';
import { ChatUserMessage } from '../ChatUserMessage/ChatUserMessage'; import { ChatUserMessage } from '../ChatUserMessage/ChatUserMessage';
import { ChatTextField } from '../ChatTextField/ChatTextField'; import { ChatTextField } from '../ChatTextField/ChatTextField';
import { ChatModeratorNotification } from '../ChatModeratorNotification/ChatModeratorNotification'; import { ChatModeratorNotification } from '../ChatModeratorNotification/ChatModeratorNotification';
// import ChatActionMessage from '../ChatAction/ChatActionMessage';
import { ChatSystemMessage } from '../ChatSystemMessage/ChatSystemMessage'; import { ChatSystemMessage } from '../ChatSystemMessage/ChatSystemMessage';
import { ChatJoinMessage } from '../ChatJoinMessage/ChatJoinMessage'; import { ChatJoinMessage } from '../ChatJoinMessage/ChatJoinMessage';
import { ScrollToBotBtn } from './ScrollToBotBtn'; import { ScrollToBotBtn } from './ScrollToBotBtn';
@ -24,6 +23,7 @@ import { ChatSocialMessage } from '../ChatSocialMessage/ChatSocialMessage';
const EditFilled = dynamic(() => import('@ant-design/icons/EditFilled'), { const EditFilled = dynamic(() => import('@ant-design/icons/EditFilled'), {
ssr: false, ssr: false,
}); });
export type ChatContainerProps = { export type ChatContainerProps = {
messages: ChatMessage[]; messages: ChatMessage[];
usernameToHighlight: string; usernameToHighlight: string;
@ -85,8 +85,28 @@ export const ChatContainer: FC<ChatContainerProps> = ({
showInput, showInput,
height, height,
}) => { }) => {
const [atBottom, setAtBottom] = useState(false); const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false);
const [isAtBottom, setIsAtBottom] = useState(false);
const chatContainerRef = useRef(null); const chatContainerRef = useRef(null);
const showScrollToBottomButtonDelay = useRef(null);
const scrollToBottomDelay = useRef(null);
const setShowScrolltoBottomButtonWithDelay = (show: boolean) => {
showScrollToBottomButtonDelay.current = setTimeout(() => {
setShowScrollToBottomButton(show);
}, 1500);
};
useEffect(
() =>
// Clear the timer when the component unmounts
() => {
clearTimeout(showScrollToBottomButtonDelay.current);
clearTimeout(scrollToBottomDelay.current);
},
[],
);
const getNameChangeViewForMessage = (message: NameChangeEvent) => { const getNameChangeViewForMessage = (message: NameChangeEvent) => {
const { oldName, user } = message; const { oldName, user } = message;
@ -127,6 +147,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
const { body } = message; const { body } = message;
return <ChatActionMessage body={body} />; return <ChatActionMessage body={body} />;
}; };
const getConnectedInfoMessage = (message: ConnectedClientInfoEvent) => { const getConnectedInfoMessage = (message: ConnectedClientInfoEvent) => {
const modStatusUpdate = checkIsModerator(message); const modStatusUpdate = checkIsModerator(message);
if (!modStatusUpdate) { if (!modStatusUpdate) {
@ -153,8 +174,8 @@ export const ChatContainer: FC<ChatContainerProps> = ({
highlightString={usernameToHighlight} // What to highlight in the message highlightString={usernameToHighlight} // What to highlight in the message
sentBySelf={(message as ChatMessage).user?.id === chatUserId} // The local user sent this message sentBySelf={(message as ChatMessage).user?.id === chatUserId} // The local user sent this message
sameUserAsLast={shouldCollapseMessages(messages, index)} sameUserAsLast={shouldCollapseMessages(messages, index)}
isAuthorModerator={(message as ChatMessage).user.scopes?.includes('MODERATOR')} isAuthorModerator={(message as ChatMessage).user?.scopes?.includes('MODERATOR')}
isAuthorBot={(message as ChatMessage).user.scopes?.includes('BOT')} isAuthorBot={(message as ChatMessage).user?.scopes?.includes('BOT')}
isAuthorAuthenticated={(message as ChatMessage).user?.authenticated} isAuthorAuthenticated={(message as ChatMessage).user?.authenticated}
key={message.id} key={message.id}
/> />
@ -187,16 +208,17 @@ export const ChatContainer: FC<ChatContainerProps> = ({
} }
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const scrollChatToBottom = (ref, behavior = 'smooth') => { const scrollChatToBottom = (ref, behavior = 'smooth') => {
setTimeout(() => { clearTimeout(scrollToBottomDelay.current);
clearTimeout(showScrollToBottomButtonDelay.current);
scrollToBottomDelay.current = setTimeout(() => {
ref.current?.scrollToIndex({ ref.current?.scrollToIndex({
index: messages.length - 1, index: messages.length - 1,
behavior, behavior,
}); });
setIsAtBottom(true);
setShowScrollToBottomButton(false);
}, 100); }, 100);
setAtBottom(true);
}; };
// This is a hack to force a scroll to the very bottom of the chat messages // This is a hack to force a scroll to the very bottom of the chat messages
@ -205,6 +227,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
scrollChatToBottom(chatContainerRef, 'auto'); scrollChatToBottom(chatContainerRef, 'auto');
setShowScrolltoBottomButtonWithDelay(false);
}, 500); }, 500);
}, []); }, []);
@ -218,22 +241,42 @@ export const ChatContainer: FC<ChatContainerProps> = ({
ref={chatContainerRef} ref={chatContainerRef}
data={messages} data={messages}
itemContent={(index, message) => getViewForMessage(index, message)} itemContent={(index, message) => getViewForMessage(index, message)}
followOutput={(isAtBottom: boolean) => { initialTopMostItemIndex={messages.length - 1}
followOutput={(atBottom: boolean) => {
console.log({ atBottom, isAtBottom });
clearTimeout(showScrollToBottomButtonDelay.current);
if (isAtBottom) { if (isAtBottom) {
scrollChatToBottom(chatContainerRef, 'smooth'); setShowScrollToBottomButton(false);
scrollChatToBottom(chatContainerRef, 'auto');
return 'smooth';
} }
setShowScrolltoBottomButtonWithDelay(true);
return false; return false;
}} }}
alignToBottom alignToBottom
atBottomThreshold={70} atBottomThreshold={70}
atBottomStateChange={bottom => { atBottomStateChange={bottom => {
setAtBottom(bottom); setIsAtBottom(bottom);
if (bottom) {
setShowScrollToBottomButton(false);
} else {
setShowScrolltoBottomButtonWithDelay(true);
}
}} }}
/> />
{!atBottom && <ScrollToBotBtn chatContainerRef={chatContainerRef} messages={messages} />} {showScrollToBottomButton && (
<ScrollToBotBtn
onClick={() => {
scrollChatToBottom(chatContainerRef, 'auto');
}}
/>
)}
</> </>
), ),
[messages, usernameToHighlight, chatUserId, isModerator, atBottom], [messages, usernameToHighlight, chatUserId, isModerator, showScrollToBottomButton, isAtBottom],
); );
return ( return (

View file

@ -1,7 +1,6 @@
import { Button } from 'antd'; import { Button } from 'antd';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { FC, MutableRefObject } from 'react'; import { FC } from 'react';
import { ChatMessage } from '../../../interfaces/chat-message.model';
import styles from './ChatContainer.module.scss'; import styles from './ChatContainer.module.scss';
// Lazy loaded components // Lazy loaded components
@ -12,23 +11,18 @@ const VerticalAlignBottomOutlined = dynamic(
ssr: false, ssr: false,
}, },
); );
type Props = { type Props = {
chatContainerRef: MutableRefObject<any>; onClick: () => void;
messages: ChatMessage[];
}; };
export const ScrollToBotBtn: FC<Props> = ({ chatContainerRef, messages }) => ( export const ScrollToBotBtn: FC<Props> = ({ onClick }) => (
<div className={styles.toBottomWrap}> <div className={styles.toBottomWrap}>
<Button <Button
type="default" type="default"
style={{ color: 'currentColor' }} style={{ color: 'currentColor' }}
icon={<VerticalAlignBottomOutlined />} icon={<VerticalAlignBottomOutlined />}
onClick={() => onClick={onClick}
chatContainerRef.current.scrollToIndex({
index: messages.length - 1,
behavior: 'auto',
})
}
> >
Go to last message Go to last message
</Button> </Button>