diff --git a/.changeset/long-geese-suffer.md b/.changeset/long-geese-suffer.md new file mode 100644 index 000000000..4d3396ff9 --- /dev/null +++ b/.changeset/long-geese-suffer.md @@ -0,0 +1,6 @@ +--- +'@livekit/components-core': patch +'@livekit/components-react': patch +--- + +Expose custom message encoder/decoder from video conference diff --git a/packages/core/etc/components-core.api.md b/packages/core/etc/components-core.api.md index 12e1ab644..aa1c18579 100644 --- a/packages/core/etc/components-core.api.md +++ b/packages/core/etc/components-core.api.md @@ -18,7 +18,7 @@ import { Observable } from 'rxjs'; import type { Participant } from 'livekit-client'; import { ParticipantEvent } from 'livekit-client'; import type { ParticipantEventCallbacks } from 'livekit-client/dist/src/room/participant/Participant'; -import { ParticipantPermission } from 'livekit-client/dist/src/proto/livekit_models'; +import { ParticipantPermission } from 'livekit-client/dist/src/proto/livekit_models_pb'; import { RemoteParticipant } from 'livekit-client'; import { Room } from 'livekit-client'; import { RoomEvent } from 'livekit-client'; @@ -347,7 +347,10 @@ export type SetMediaDeviceOptions = { }; // @public (undocumented) -export function setupChat(room: Room): { +export function setupChat(room: Room, options?: { + messageEncoder?: (message: ChatMessage) => Uint8Array; + messageDecoder?: (message: Uint8Array) => ReceivedChatMessage; +}): { messageObservable: Observable; isSendingObservable: BehaviorSubject; send: (message: string) => Promise; diff --git a/packages/core/src/components/chat.ts b/packages/core/src/components/chat.ts index bb4b7c0d5..5063ee1a9 100644 --- a/packages/core/src/components/chat.ts +++ b/packages/core/src/components/chat.ts @@ -16,7 +16,18 @@ export interface ReceivedChatMessage extends ChatMessage { const encoder = new TextEncoder(); const decoder = new TextDecoder(); -export function setupChat(room: Room) { +const encode = (message: ChatMessage) => + encoder.encode(JSON.stringify({ message: message.message, timestamp: message.timestamp })); + +const decode = (message: Uint8Array) => JSON.parse(decoder.decode(message)) as ReceivedChatMessage; + +export function setupChat( + room: Room, + options?: { + messageEncoder?: (message: ChatMessage) => Uint8Array; + messageDecoder?: (message: Uint8Array) => ReceivedChatMessage; + }, +) { const onDestroyObservable = new Subject(); const messageSubject = new Subject<{ payload: Uint8Array; @@ -28,10 +39,14 @@ export function setupChat(room: Room) { const { messageObservable } = setupDataMessageHandler(room, DataTopic.CHAT); messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject); + const { messageDecoder, messageEncoder } = options ?? {}; + + const finalMessageDecoder = messageDecoder ?? decode; + /** Build up the message array over time. */ const messagesObservable = messageSubject.pipe( map((msg) => { - const parsedMessage = JSON.parse(decoder.decode(msg.payload)) as ChatMessage; + const parsedMessage = finalMessageDecoder(msg.payload); const newMessage: ReceivedChatMessage = { ...parsedMessage, from: msg.from }; return newMessage; }), @@ -41,9 +56,11 @@ export function setupChat(room: Room) { const isSending$ = new BehaviorSubject(false); + const finalMessageEncoder = messageEncoder ?? encode; + const send = async (message: string) => { const timestamp = Date.now(); - const encodedMsg = encoder.encode(JSON.stringify({ timestamp, message })); + const encodedMsg = finalMessageEncoder({ message, timestamp }); isSending$.next(true); try { await sendMessage(room.localParticipant, encodedMsg, DataTopic.CHAT, { diff --git a/packages/react/etc/components-react.api.md b/packages/react/etc/components-react.api.md index e2b2504a3..7d153acc4 100644 --- a/packages/react/etc/components-react.api.md +++ b/packages/react/etc/components-react.api.md @@ -101,7 +101,7 @@ export interface CarouselViewProps extends React_2.HTMLAttributes { + // (undocumented) + messageDecoder?: MessageDecoder; + // (undocumented) + messageEncoder?: MessageEncoder; // (undocumented) messageFormatter?: MessageFormatter; } @@ -336,6 +340,12 @@ export interface MediaDeviceSelectProps extends React_2.HTMLAttributes ReceivedChatMessage; + +// @public (undocumented) +export type MessageEncoder = (message: ChatMessage) => Uint8Array; + // @public (undocumented) export type MessageFormatter = (message: string) => React_2.ReactNode; @@ -476,7 +486,10 @@ export function useAudioPlayback(room?: Room): { }; // @public (undocumented) -export function useChat(): { +export function useChat(options?: { + messageEncoder?: MessageEncoder; + messageDecoder?: MessageDecoder; +}): { send: ((message: string) => Promise) | undefined; chatMessages: ReceivedChatMessage[]; isSending: boolean; @@ -902,10 +915,14 @@ export interface UseVisualStableUpdateOptions { } // @public -export function VideoConference({ chatMessageFormatter, ...props }: VideoConferenceProps): React_2.JSX.Element; +export function VideoConference({ chatMessageFormatter, chatMessageDecoder, chatMessageEncoder, ...props }: VideoConferenceProps): React_2.JSX.Element; // @public (undocumented) export interface VideoConferenceProps extends React_2.HTMLAttributes { + // (undocumented) + chatMessageDecoder?: MessageDecoder; + // (undocumented) + chatMessageEncoder?: MessageEncoder; // (undocumented) chatMessageFormatter?: MessageFormatter; } diff --git a/packages/react/src/components/ChatEntry.tsx b/packages/react/src/components/ChatEntry.tsx index 16b4f2e47..c1103e5a2 100644 --- a/packages/react/src/components/ChatEntry.tsx +++ b/packages/react/src/components/ChatEntry.tsx @@ -1,10 +1,15 @@ -import type { ReceivedChatMessage } from '@livekit/components-core'; +import type { ReceivedChatMessage, ChatMessage } from '@livekit/components-core'; import { tokenize, createDefaultGrammar } from '@livekit/components-core'; import * as React from 'react'; /** @public */ export type MessageFormatter = (message: string) => React.ReactNode; +/** @public */ +export type MessageEncoder = (message: ChatMessage) => Uint8Array; +/** @public */ +export type MessageDecoder = (message: Uint8Array) => ReceivedChatMessage; + /** * ChatEntry composes the HTML div element under the hood, so you can pass all its props. * These are the props specific to the ChatEntry component: diff --git a/packages/react/src/prefabs/Chat.tsx b/packages/react/src/prefabs/Chat.tsx index 1154e5f27..b2ca5f5e7 100644 --- a/packages/react/src/prefabs/Chat.tsx +++ b/packages/react/src/prefabs/Chat.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useMaybeLayoutContext, useRoomContext } from '../context'; import { useObservableState } from '../hooks/internal/useObservableState'; import { cloneSingleChild } from '../utils'; -import type { MessageFormatter } from '../components/ChatEntry'; +import type { MessageDecoder, MessageEncoder, MessageFormatter } from '../components/ChatEntry'; import { ChatEntry } from '../components/ChatEntry'; export type { ChatMessage, ReceivedChatMessage }; @@ -12,20 +12,25 @@ export type { ChatMessage, ReceivedChatMessage }; /** @public */ export interface ChatProps extends React.HTMLAttributes { messageFormatter?: MessageFormatter; + messageEncoder?: MessageEncoder; + messageDecoder?: MessageDecoder; } /** @public */ -export function useChat() { +export function useChat(options?: { + messageEncoder?: MessageEncoder; + messageDecoder?: MessageDecoder; +}) { 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); + const setupChatReturn = setupChat(room, options); setSetup(setupChatReturn); return setupChatReturn.destroy; - }, [room]); + }, [room, options]); return { send: setup?.send, chatMessages, isSending }; } @@ -42,10 +47,16 @@ export function useChat() { * ``` * @public */ -export function Chat({ messageFormatter, ...props }: ChatProps) { +export function Chat({ messageFormatter, messageDecoder, messageEncoder, ...props }: ChatProps) { const inputRef = React.useRef(null); const ulRef = React.useRef(null); - const { send, chatMessages, isSending } = useChat(); + + const chatOptions = React.useMemo(() => { + return { messageDecoder, messageEncoder }; + }, [messageDecoder, messageEncoder]); + + const { send, chatMessages, isSending } = useChat(chatOptions); + const layoutContext = useMaybeLayoutContext(); const lastReadMsgAt = React.useRef(0); diff --git a/packages/react/src/prefabs/VideoConference.tsx b/packages/react/src/prefabs/VideoConference.tsx index c48527c9b..144fced7a 100644 --- a/packages/react/src/prefabs/VideoConference.tsx +++ b/packages/react/src/prefabs/VideoConference.tsx @@ -8,7 +8,7 @@ import type { WidgetState } from '@livekit/components-core'; import { isEqualTrackRef, isTrackReference, log, isWeb } from '@livekit/components-core'; import { Chat } from './Chat'; import { ConnectionStateToast } from '../components/Toast'; -import type { MessageFormatter } from '../components/ChatEntry'; +import type { MessageDecoder, MessageEncoder, MessageFormatter } from '../components/ChatEntry'; import { RoomEvent, Track } from 'livekit-client'; import { useTracks } from '../hooks/useTracks'; import { usePinnedTracks } from '../hooks/usePinnedTracks'; @@ -22,6 +22,8 @@ import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; */ export interface VideoConferenceProps extends React.HTMLAttributes { chatMessageFormatter?: MessageFormatter; + chatMessageEncoder?: MessageEncoder; + chatMessageDecoder?: MessageDecoder; } /** @@ -40,7 +42,12 @@ export interface VideoConferenceProps extends React.HTMLAttributes({ showChat: false, unreadMessages: 0, @@ -122,6 +129,8 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere )}