import { type ChatMessage, type ChatOptions } from '@livekit/components-core'; import * as React from 'react'; import { useMaybeLayoutContext } from '../context'; import { cloneSingleChild } from '../utils'; import type { MessageFormatter } from '../components/ChatEntry'; import { ChatEntry } from '../components/ChatEntry'; import { useChat } from '../hooks/useChat'; import { ChatToggle } from '../components'; import ChatCloseIcon from '../assets/icons/ChatCloseIcon'; /** @public */ export interface ChatProps extends React.HTMLAttributes, ChatOptions { messageFormatter?: MessageFormatter; } /** * The Chat component provides ready-to-use chat functionality in a LiveKit room. * Messages are distributed to all participants in the room in real-time. * * @remarks * - Only users who are in the room at the time of dispatch will receive messages * - Message history is not persisted between sessions * - Requires `@livekit/components-styles` to be imported for styling * * @example * ```tsx * import '@livekit/components-styles'; * * function Room() { * return ( * * * * ); * } * ``` * * For custom styling, refer to: https://docs.livekit.io/reference/components/react/concepts/style-components/ * * @public */ export function Chat({ messageFormatter, messageDecoder, messageEncoder, channelTopic, ...props }: ChatProps) { const ulRef = React.useRef(null); const inputRef = React.useRef(null); const chatOptions: ChatOptions = React.useMemo(() => { return { messageDecoder, messageEncoder, channelTopic }; }, [messageDecoder, messageEncoder, channelTopic]); const { chatMessages, send, isSending } = useChat(chatOptions); const layoutContext = useMaybeLayoutContext(); const lastReadMsgAt = React.useRef(0); async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (inputRef.current && inputRef.current.value.trim() !== '') { await send(inputRef.current.value); inputRef.current.value = ''; inputRef.current.focus(); } } React.useEffect(() => { if (ulRef) { ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight }); } }, [ulRef, chatMessages]); React.useEffect(() => { if (!layoutContext || chatMessages.length === 0) { return; } if ( layoutContext.widget.state?.showChat && chatMessages.length > 0 && lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp ) { lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp; return; } const unreadMessageCount = chatMessages.filter( (msg) => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current, ).length; const { widget } = layoutContext; if (unreadMessageCount > 0 && widget.state?.unreadMessages !== unreadMessageCount) { widget.dispatch?.({ msg: 'unread_msg', count: unreadMessageCount }); } }, [chatMessages, layoutContext?.widget]); return (
Messages {layoutContext && ( )}
    {props.children ? chatMessages.map((msg, idx) => cloneSingleChild(props.children, { entry: msg, key: msg.id ?? idx, messageFormatter, }), ) : chatMessages.map((msg, idx, allMsg) => { const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from; // If the time delta between two messages is bigger than 60s show timestamp. const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000; return ( ); })}
ev.stopPropagation()} onKeyDown={(ev) => ev.stopPropagation()} onKeyUp={(ev) => ev.stopPropagation()} />
); }