{
- const { dispatch, state } = useLayoutContext().widget
-
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == SidePanel.PARTICIPANTS
const isEffectsOpen = sidePanel == SidePanel.EFFECTS
+ const isChatOpen = sidePanel == SidePanel.CHAT
const toggleParticipants = () => {
- if (dispatch && state?.showChat) {
- dispatch({ msg: 'toggle_chat' })
- }
layoutStore.sidePanel = isParticipantsOpen ? null : SidePanel.PARTICIPANTS
}
const toggleChat = () => {
- if (isParticipantsOpen || isEffectsOpen) {
- layoutStore.sidePanel = null
- }
- if (dispatch) {
- dispatch({ msg: 'toggle_chat' })
- }
+ layoutStore.sidePanel = isChatOpen ? null : SidePanel.CHAT
}
const toggleEffects = () => {
- if (dispatch && state?.showChat) {
- dispatch({ msg: 'toggle_chat' })
- }
layoutStore.sidePanel = isEffectsOpen ? null : SidePanel.EFFECTS
}
@@ -43,8 +31,7 @@ export const useWidgetInteraction = () => {
toggleParticipants,
toggleChat,
toggleEffects,
- isChatOpen: state?.showChat,
- unreadMessages: state?.unreadMessages,
+ isChatOpen,
isParticipantsOpen,
isEffectsOpen,
}
diff --git a/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx b/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx
index 5b091628..ea79d918 100644
--- a/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx
+++ b/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx
@@ -1,42 +1,40 @@
import type { ChatMessage, ChatOptions } from '@livekit/components-core'
import * as React from 'react'
import {
- ChatCloseIcon,
- ChatEntry,
- ChatToggle,
- MessageFormatter,
+ formatChatMessageLinks,
useChat,
- useMaybeLayoutContext,
+ useParticipants,
} from '@livekit/components-react'
-import { cloneSingleChild } from '@/features/rooms/utils/cloneSingleChild'
-import { ChatInput } from '@/features/rooms/livekit/components/chat/Input'
+import { useTranslation } from 'react-i18next'
+import { useSnapshot } from 'valtio'
+import { chatStore } from '@/stores/chat'
+import { Div, Text } from '@/primitives'
+import { ChatInput } from '../components/chat/Input'
+import { ChatEntry } from '../components/chat/Entry'
+import { useWidgetInteraction } from '../hooks/useWidgetInteraction'
-/** @public */
export interface ChatProps
extends React.HTMLAttributes
,
- ChatOptions {
- messageFormatter?: MessageFormatter
-}
+ ChatOptions {}
/**
* 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 function Chat({ messageFormatter, ...props }: ChatProps) {
+export function Chat({ ...props }: ChatProps) {
+ const { t } = useTranslation('rooms', { keyPrefix: 'chat' })
+
const inputRef = React.useRef(null)
const ulRef = React.useRef(null)
const { send, chatMessages, isSending } = useChat()
- const layoutContext = useMaybeLayoutContext()
+ const { isChatOpen } = useWidgetInteraction()
+ const chatSnap = useSnapshot(chatStore)
+
+ // Use useParticipants hook to trigger a re-render when the participant list changes.
+ const participants = useParticipants()
+
const lastReadMsgAt = React.useRef(0)
async function handleSubmit(text: string) {
@@ -46,22 +44,21 @@ export function Chat({ messageFormatter, ...props }: ChatProps) {
}
React.useEffect(() => {
- if (ulRef) {
+ if (chatMessages.length > 0 && ulRef.current) {
ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight })
}
}, [ulRef, chatMessages])
React.useEffect(() => {
- if (!layoutContext || chatMessages.length === 0) {
+ if (chatMessages.length === 0) {
return
}
-
if (
- layoutContext.widget.state?.showChat &&
- chatMessages.length > 0 &&
+ isChatOpen &&
lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp
) {
lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp
+ chatStore.unreadMessages = 0
return
}
@@ -69,55 +66,69 @@ export function Chat({ messageFormatter, ...props }: ChatProps) {
(msg) => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current
).length
- const { widget } = layoutContext
if (
unreadMessageCount > 0 &&
- widget.state?.unreadMessages !== unreadMessageCount
+ chatSnap.unreadMessages !== unreadMessageCount
) {
- widget.dispatch?.({ msg: 'unread_msg', count: unreadMessageCount })
+ chatStore.unreadMessages = unreadMessageCount
}
- }, [chatMessages, layoutContext, layoutContext?.widget])
+ }, [chatMessages, chatSnap.unreadMessages, isChatOpen])
+
+ const renderedMessages = React.useMemo(() => {
+ return chatMessages.map((msg, idx, allMsg) => {
+ const hideMetadata =
+ idx >= 1 &&
+ msg.timestamp - allMsg[idx - 1].timestamp < 60_000 &&
+ allMsg[idx - 1].from === msg.from
+
+ return (
+
+ )
+ })
+ // This ensures that the chat message list is updated to reflect any changes in participant information.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chatMessages, participants])
return (
-
-
- Messages
-
-
-
-
-
-
- {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 (
-
- )
- })}
-
+
+
+ {t('disclaimer')}
+
+
handleSubmit(e)}
isSending={isSending}
/>
-
+
)
}
diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
index db3609ad..8a1d8b69 100644
--- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
+++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
@@ -1,6 +1,4 @@
-import type {
- TrackReferenceOrPlaceholder,
-} from '@livekit/components-core'
+import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import {
isEqualTrackRef,
isTrackReference,
@@ -17,7 +15,6 @@ import {
GridLayout,
LayoutContextProvider,
RoomAudioRenderer,
- MessageFormatter,
usePinnedTracks,
useTracks,
useCreateLayoutContext,
@@ -50,7 +47,6 @@ const LayoutWrapper = styled(
*/
export interface VideoConferenceProps
extends React.HTMLAttributes {
- chatMessageFormatter?: MessageFormatter
/** @alpha */
SettingsComponent?: React.ComponentType
}
@@ -73,10 +69,7 @@ export interface VideoConferenceProps
* ```
* @public
*/
-export function VideoConference({
- chatMessageFormatter,
- ...props
-}: VideoConferenceProps) {
+export function VideoConference({ ...props }: VideoConferenceProps) {
const lastAutoFocusedScreenShareTrack =
React.useRef(null)
diff --git a/src/frontend/src/features/rooms/utils/cloneSingleChild.ts b/src/frontend/src/features/rooms/utils/cloneSingleChild.ts
deleted file mode 100644
index 06b618a5..00000000
--- a/src/frontend/src/features/rooms/utils/cloneSingleChild.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-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,
- // 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
- })
-}
diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json
index ec940b1e..313d98ce 100644
--- a/src/frontend/src/locales/de/rooms.json
+++ b/src/frontend/src/locales/de/rooms.json
@@ -97,14 +97,19 @@
"sidePanel": {
"heading": {
"participants": "",
- "effects": ""
+ "effects": "",
+ "chat": ""
},
"content": {
"participants": "",
- "effects": ""
+ "effects": "",
+ "chat": ""
},
"closeButton": ""
},
+ "chat": {
+ "disclaimer": ""
+ },
"participants": {
"subheading": "",
"contributors": "",
diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json
index c512a02d..62048bc6 100644
--- a/src/frontend/src/locales/en/rooms.json
+++ b/src/frontend/src/locales/en/rooms.json
@@ -95,14 +95,19 @@
"sidePanel": {
"heading": {
"participants": "Participants",
- "effects": "Effects"
+ "effects": "Effects",
+ "chat": "Messages in the chat"
},
"content": {
"participants": "participants",
- "effects": "effects"
+ "effects": "effects",
+ "chat": "messages"
},
"closeButton": "Hide {{content}}"
},
+ "chat": {
+ "disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
+ },
"participants": {
"subheading": "In room",
"you": "You",
diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json
index 5e92fd17..4596409d 100644
--- a/src/frontend/src/locales/fr/rooms.json
+++ b/src/frontend/src/locales/fr/rooms.json
@@ -95,14 +95,19 @@
"sidePanel": {
"heading": {
"participants": "Participants",
- "effects": "Effets"
+ "effects": "Effets",
+ "chat": "Messages dans l'appel"
},
"content": {
"participants": "les participants",
- "effects": "les effets"
+ "effects": "les effets",
+ "chat": "les messages"
},
"closeButton": "Masquer {{content}}"
},
+ "chat": {
+ "disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
+ },
"participants": {
"subheading": "Dans la réunion",
"you": "Vous",
diff --git a/src/frontend/src/primitives/TextArea.tsx b/src/frontend/src/primitives/TextArea.tsx
index b59c7222..e7f1d2d0 100644
--- a/src/frontend/src/primitives/TextArea.tsx
+++ b/src/frontend/src/primitives/TextArea.tsx
@@ -15,4 +15,13 @@ export const TextArea = styled(RACTextArea, {
borderRadius: 4,
transition: 'all 200ms',
},
+ variants: {
+ placeholderStyle: {
+ strong: {
+ _placeholder: {
+ color: 'black',
+ },
+ },
+ },
+ },
})
diff --git a/src/frontend/src/stores/chat.ts b/src/frontend/src/stores/chat.ts
new file mode 100644
index 00000000..e4664ad7
--- /dev/null
+++ b/src/frontend/src/stores/chat.ts
@@ -0,0 +1,9 @@
+import { proxy } from 'valtio'
+
+type State = {
+ unreadMessages: number
+}
+
+export const chatStore = proxy({
+ unreadMessages: 0,
+})