mirror of
https://github.com/owncast/owncast.git
synced 2024-11-29 11:39:08 +03:00
Improve chat container bottom scrolling handling. Closes #2342
This commit is contained in:
parent
835a644de0
commit
564d058e2e
2 changed files with 61 additions and 24 deletions
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue