import * as React from 'react' import type { ChatMessage, ReceivedChatMessage } from '@livekit/components-core' import { MessageFormatter, useLocalParticipant, useMaybeLayoutContext, useRoomContext } from '@livekit/components-react' import ChatEntry from './ChatEntry' import { cloneSingleChild, setupChat, useObservableState } from './utils' import styles from './Chat.module.css' export type { ChatMessage, ReceivedChatMessage } export interface ChatProps extends React.HTMLAttributes { messageFormatter?: MessageFormatter } /** @public */ export function useChat() { const room = useRoomContext() const [setup, setSetup] = React.useState>() const isSending = useObservableState(setup?.isSendingObservable, false) const chatMessages = useObservableState(setup?.messageObservable, []) React.useEffect(() => { const setupChatReturn = setupChat(room) setSetup(setupChatReturn) return setupChatReturn.destroy }, [room]) return { send: setup?.send, chatMessages, isSending } } /** * The Chat component adds a basis chat functionality to the LiveKit room. The messages are distributed to all participants * in the room. Only users who are in the room at the time of dispatch will receive the message. * * @example * ```tsx * * * * ``` * @public */ export default function Chat({ messageFormatter, ...props }: ChatProps) { const inputRef = React.useRef(null) const ulRef = React.useRef(null) const { send, chatMessages, isSending } = useChat() const layoutContext = useMaybeLayoutContext() const lastReadMsgAt = React.useRef(0) async function handleSubmit(event: React.FormEvent) { event.preventDefault() if (inputRef.current && inputRef.current.value.trim() !== '') { if (send) { 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]) const localParticipant = useLocalParticipant().localParticipant return (
    {props.children ? chatMessages.map((msg, idx) => cloneSingleChild(props.children, { entry: msg, key: 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 ( ) })}
{localParticipant.permissions?.canPublish && (
)}
) }