💩(frontend) duplicate chat prefabs component

Duplicate LiveKit component to start customizing it.
This commit is contained in:
lebaudantoine
2024-10-09 17:19:49 +02:00
committed by aleb_the_flash
parent 1875a394c6
commit 70ed99b6c9
3 changed files with 178 additions and 1 deletions

View File

@@ -0,0 +1,151 @@
import type { ChatMessage, ChatOptions } from '@livekit/components-core'
import * as React from 'react'
import {
ChatCloseIcon,
ChatEntry,
ChatToggle,
MessageFormatter,
useChat,
useMaybeLayoutContext,
} from '@livekit/components-react'
import { cloneSingleChild } from '@/features/rooms/utils/cloneSingleChild'
/** @public */
export interface ChatProps
extends React.HTMLAttributes<HTMLDivElement>,
ChatOptions {
messageFormatter?: MessageFormatter
}
/**
* 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
* <LiveKitRoom>
* <Chat />
* </LiveKitRoom>
* ```
* @public
*/
export function Chat({
messageFormatter,
messageDecoder,
messageEncoder,
channelTopic,
...props
}: ChatProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const ulRef = React.useRef<HTMLUListElement>(null)
const chatOptions: ChatOptions = React.useMemo(() => {
return { messageDecoder, messageEncoder, channelTopic }
}, [messageDecoder, messageEncoder, channelTopic])
const { send, chatMessages, isSending } = useChat(chatOptions)
const layoutContext = useMaybeLayoutContext()
const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(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, layoutContext?.widget])
return (
<div {...props} className="lk-chat">
<div className="lk-chat-header">
Messages
<ChatToggle className="lk-close-button">
<ChatCloseIcon />
</ChatToggle>
</div>
<ul className="lk-list lk-chat-messages" ref={ulRef}>
{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 (
<ChatEntry
key={msg.id ?? idx}
hideName={hideName}
hideTimestamp={hideName === false ? false : hideTimestamp} // If we show the name always show the timestamp as well.
entry={msg}
messageFormatter={messageFormatter}
/>
)
})}
</ul>
<form className="lk-chat-form" onSubmit={handleSubmit}>
<input
className="lk-form-control lk-chat-form-input"
disabled={isSending}
ref={inputRef}
type="text"
placeholder="Enter a message..."
onInput={(ev) => ev.stopPropagation()}
onKeyDown={(ev) => ev.stopPropagation()}
onKeyUp={(ev) => ev.stopPropagation()}
/>
<button
type="submit"
className="lk-button lk-chat-form-button"
disabled={isSending}
>
Send
</button>
</form>
</div>
)
}

View File

@@ -24,7 +24,6 @@ import {
usePinnedTracks,
useTracks,
useCreateLayoutContext,
Chat,
} from '@livekit/components-react'
import { ControlBar } from './ControlBar'
@@ -36,6 +35,7 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
import { Chat } from '@/features/rooms/livekit/prefabs/Chat'
const LayoutWrapper = styled(
'div',

View File

@@ -0,0 +1,26 @@
import React from 'react'
import clsx from 'clsx'
/** @internal function from LiveKit*/
export function cloneSingleChild(
children: React.ReactNode | React.ReactNode[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props?: Record<string, any>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
key?: any
) {
return React.Children.map(children, (child) => {
// Checking isValidElement is the safe way and avoids a typescript
// error too.
if (React.isValidElement(child) && React.Children.only(children)) {
if (child.props.class) {
// make sure we retain classnames of both passed props and child
props ??= {}
props.class = clsx(child.props.class, props.class)
props.style = { ...child.props.style, ...props.style }
}
return React.cloneElement(child, { ...props, key })
}
return child
})
}