💩(frontend) duplicate chat prefabs component
Duplicate LiveKit component to start customizing it.
This commit is contained in:
committed by
aleb_the_flash
parent
1875a394c6
commit
70ed99b6c9
151
src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx
Normal file
151
src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
26
src/frontend/src/features/rooms/utils/cloneSingleChild.ts
Normal file
26
src/frontend/src/features/rooms/utils/cloneSingleChild.ts
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user