Skip to content

Commit

Permalink
Expose custom message encoder/decoder from video conference (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmoguilevsky authored Aug 9, 2023
1 parent fa3060b commit f1ed9cf
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 17 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-geese-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@livekit/components-core': patch
'@livekit/components-react': patch
---

Expose custom message encoder/decoder from video conference
7 changes: 5 additions & 2 deletions packages/core/etc/components-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ReceivedChatMessage[]>;
isSendingObservable: BehaviorSubject<boolean>;
send: (message: string) => Promise<void>;
Expand Down
23 changes: 20 additions & 3 deletions packages/core/src/components/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
const messageSubject = new Subject<{
payload: Uint8Array;
Expand All @@ -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;
}),
Expand All @@ -41,9 +56,11 @@ export function setupChat(room: Room) {

const isSending$ = new BehaviorSubject<boolean>(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, {
Expand Down
23 changes: 20 additions & 3 deletions packages/react/etc/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export interface CarouselViewProps extends React_2.HTMLAttributes<HTMLMediaEleme
}

// @public
export function Chat({ messageFormatter, ...props }: ChatProps): React_2.JSX.Element;
export function Chat({ messageFormatter, messageDecoder, messageEncoder, ...props }: ChatProps): React_2.JSX.Element;

// @public
export function ChatEntry({ entry, hideName, hideTimestamp, messageFormatter, ...props }: ChatEntryProps): React_2.JSX.Element;
Expand All @@ -118,6 +118,10 @@ export { ChatMessage }

// @public (undocumented)
export interface ChatProps extends React_2.HTMLAttributes<HTMLDivElement> {
// (undocumented)
messageDecoder?: MessageDecoder;
// (undocumented)
messageEncoder?: MessageEncoder;
// (undocumented)
messageFormatter?: MessageFormatter;
}
Expand Down Expand Up @@ -336,6 +340,12 @@ export interface MediaDeviceSelectProps extends React_2.HTMLAttributes<HTMLUList
track?: LocalAudioTrack | LocalVideoTrack;
}

// @public (undocumented)
export type MessageDecoder = (message: Uint8Array) => ReceivedChatMessage;

// @public (undocumented)
export type MessageEncoder = (message: ChatMessage) => Uint8Array;

// @public (undocumented)
export type MessageFormatter = (message: string) => React_2.ReactNode;

Expand Down Expand Up @@ -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<void>) | undefined;
chatMessages: ReceivedChatMessage[];
isSending: boolean;
Expand Down Expand Up @@ -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<HTMLDivElement> {
// (undocumented)
chatMessageDecoder?: MessageDecoder;
// (undocumented)
chatMessageEncoder?: MessageEncoder;
// (undocumented)
chatMessageFormatter?: MessageFormatter;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/components/ChatEntry.tsx
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
23 changes: 17 additions & 6 deletions packages/react/src/prefabs/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,33 @@ 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 };

/** @public */
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
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<ReturnType<typeof setupChat>>();
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 };
}
Expand All @@ -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<HTMLInputElement>(null);
const ulRef = React.useRef<HTMLUListElement>(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<ChatMessage['timestamp']>(0);

Expand Down
13 changes: 11 additions & 2 deletions packages/react/src/prefabs/VideoConference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,6 +22,8 @@ import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
*/
export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElement> {
chatMessageFormatter?: MessageFormatter;
chatMessageEncoder?: MessageEncoder;
chatMessageDecoder?: MessageDecoder;
}

/**
Expand All @@ -40,7 +42,12 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
* ```
* @public
*/
export function VideoConference({ chatMessageFormatter, ...props }: VideoConferenceProps) {
export function VideoConference({
chatMessageFormatter,
chatMessageDecoder,
chatMessageEncoder,
...props
}: VideoConferenceProps) {
const [widgetState, setWidgetState] = React.useState<WidgetState>({
showChat: false,
unreadMessages: 0,
Expand Down Expand Up @@ -122,6 +129,8 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere
<Chat
style={{ display: widgetState.showChat ? 'flex' : 'none' }}
messageFormatter={chatMessageFormatter}
messageEncoder={chatMessageEncoder}
messageDecoder={chatMessageDecoder}
/>
</LayoutContextProvider>
)}
Expand Down

0 comments on commit f1ed9cf

Please sign in to comment.