Skip to content

Commit

Permalink
Add support for multiple chat windows (#713)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasIO authored Nov 22, 2023
1 parent 2dab3b0 commit adc3d04
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 51 deletions.
6 changes: 6 additions & 0 deletions .changeset/pink-spoons-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@livekit/components-core": patch
"@livekit/components-react": patch
---

Add support for multiple chat windows
13 changes: 8 additions & 5 deletions packages/core/etc/components-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export interface ChatMessage {
timestamp: number;
}

// @public (undocumented)
export type ChatOptions = {
messageEncoder?: (message: ChatMessage) => Uint8Array;
messageDecoder?: (message: Uint8Array) => ReceivedChatMessage;
channelTopic?: string;
};

// @public (undocumented)
export function computeMenuPosition(button: HTMLElement, menu: HTMLElement): Promise<{
x: number;
Expand Down Expand Up @@ -387,14 +394,10 @@ export type SetMediaDeviceOptions = {
};

// @public (undocumented)
export function setupChat(room: Room, options?: {
messageEncoder?: (message: ChatMessage) => Uint8Array;
messageDecoder?: (message: Uint8Array) => ReceivedChatMessage;
}): {
export function setupChat(room: Room, options?: ChatOptions): {
messageObservable: Observable<ReceivedChatMessage[]>;
isSendingObservable: BehaviorSubject<boolean>;
send: (message: string) => Promise<void>;
destroy: () => void;
};

// @public (undocumented)
Expand Down
52 changes: 31 additions & 21 deletions packages/core/src/components/chat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import type { Participant, Room } from 'livekit-client';
import { DataPacket_Kind } from 'livekit-client';
import { DataPacket_Kind, RoomEvent } from 'livekit-client';
import { BehaviorSubject, Subject, scan, map, takeUntil } from 'rxjs';
import { DataTopic, sendMessage, setupDataMessageHandler } from '../observables/dataChannel';

Expand All @@ -19,34 +19,42 @@ export interface ReceivedChatMessage extends ChatMessage {
export type MessageEncoder = (message: ChatMessage) => Uint8Array;
/** @public */
export type MessageDecoder = (message: Uint8Array) => ReceivedChatMessage;
/** @public */
export type ChatOptions = {
messageEncoder?: (message: ChatMessage) => Uint8Array;
messageDecoder?: (message: Uint8Array) => ReceivedChatMessage;
channelTopic?: string;
};

type RawMessage = {
payload: Uint8Array;
topic: string | undefined;
from: Participant | undefined;
};

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const topicSubjectMap: Map<string, Subject<RawMessage>> = new Map();

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;
},
) {
export function setupChat(room: Room, options?: ChatOptions) {
const onDestroyObservable = new Subject<void>();
const messageSubject = new Subject<{
payload: Uint8Array;
topic: string | undefined;
from: Participant | undefined;
}>();

/** Subscribe to all messages send over the wire. */
const { messageObservable } = setupDataMessageHandler(room, DataTopic.CHAT);
messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject);

const { messageDecoder, messageEncoder } = options ?? {};
const { messageDecoder, messageEncoder, channelTopic } = options ?? {};

const topic = channelTopic ?? DataTopic.CHAT;

const messageSubject = topicSubjectMap.get(topic) ?? new Subject<RawMessage>();
topicSubjectMap.set(topic, messageSubject);

/** Subscribe to all appropriate messages sent over the wire. */
const { messageObservable } = setupDataMessageHandler(room, topic);
messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject);

const finalMessageDecoder = messageDecoder ?? decode;

Expand All @@ -70,12 +78,12 @@ export function setupChat(
const encodedMsg = finalMessageEncoder({ message, timestamp });
isSending$.next(true);
try {
await sendMessage(room.localParticipant, encodedMsg, DataTopic.CHAT, {
await sendMessage(room.localParticipant, encodedMsg, topic, {
kind: DataPacket_Kind.RELIABLE,
});
messageSubject.next({
payload: encodedMsg,
topic: DataTopic.CHAT,
topic: topic,
from: room.localParticipant,
});
} finally {
Expand All @@ -86,7 +94,9 @@ export function setupChat(
function destroy() {
onDestroyObservable.next();
onDestroyObservable.complete();
topicSubjectMap.clear();
}
room.once(RoomEvent.Disconnected, destroy);

return { messageObservable: messagesObservable, isSendingObservable: isSending$, send, destroy };
return { messageObservable: messagesObservable, isSendingObservable: isSending$, send };
}
15 changes: 5 additions & 10 deletions packages/react/etc/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export interface CarouselLayoutProps extends React_2.HTMLAttributes<HTMLMediaEle
export const CarouselView: typeof CarouselLayout;

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

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

// Warning: (ae-forgotten-export) The symbol "ChatOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export interface ChatProps extends React_2.HTMLAttributes<HTMLDivElement> {
// (undocumented)
messageDecoder?: MessageDecoder;
// (undocumented)
messageEncoder?: MessageEncoder;
export interface ChatProps extends React_2.HTMLAttributes<HTMLDivElement>, ChatOptions {
// (undocumented)
messageFormatter?: MessageFormatter;
}
Expand Down Expand Up @@ -672,10 +670,7 @@ export function useAudioPlayback(room?: Room): {
};

// @public
export function useChat(options?: {
messageEncoder?: MessageEncoder;
messageDecoder?: MessageDecoder;
}): {
export function useChat(options?: ChatOptions): {
send: ((message: string) => Promise<void>) | undefined;
chatMessages: ReceivedChatMessage[];
isSending: boolean;
Expand Down
10 changes: 3 additions & 7 deletions packages/react/src/hooks/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MessageDecoder, MessageEncoder } from '@livekit/components-core';
import { ReceivedChatMessage, setupChat } from '@livekit/components-core';
import type { ChatOptions, ReceivedChatMessage } from '@livekit/components-core';
import { setupChat } from '@livekit/components-core';
import * as React from 'react';
import { useRoomContext } from '../context';
import { useObservableState } from './internal/useObservableState';
Expand All @@ -11,10 +11,7 @@ import { useObservableState } from './internal/useObservableState';
* It is possible to pass configurations for custom message encoding and decoding.
* @public
*/
export function useChat(options?: {
messageEncoder?: MessageEncoder;
messageDecoder?: MessageDecoder;
}) {
export function useChat(options?: ChatOptions) {
const room = useRoomContext();
const [setup, setSetup] = React.useState<ReturnType<typeof setupChat>>();
const isSending = useObservableState(setup?.isSendingObservable, false);
Expand All @@ -23,7 +20,6 @@ export function useChat(options?: {
React.useEffect(() => {
const setupChatReturn = setupChat(room, options);
setSetup(setupChatReturn);
return setupChatReturn.destroy;
}, [room, options]);

return { send: setup?.send, chatMessages, isSending };
Expand Down
20 changes: 12 additions & 8 deletions packages/react/src/prefabs/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChatMessage, MessageEncoder, MessageDecoder } from '@livekit/components-core';
import type { ChatMessage, ChatOptions } from '@livekit/components-core';
import * as React from 'react';
import { useMaybeLayoutContext } from '../context';
import { cloneSingleChild } from '../utils';
Expand All @@ -7,10 +7,8 @@ import { ChatEntry } from '../components/ChatEntry';
import { useChat } from '../hooks/useChat';

/** @public */
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {
messageFormatter?: MessageFormatter;
messageEncoder?: MessageEncoder;
messageDecoder?: MessageDecoder;
}

/**
Expand All @@ -25,13 +23,19 @@ export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
* ```
* @public
*/
export function Chat({ messageFormatter, messageDecoder, messageEncoder, ...props }: ChatProps) {
export function Chat({
messageFormatter,
messageDecoder,
messageEncoder,
channelTopic,
...props
}: ChatProps) {
const inputRef = React.useRef<HTMLInputElement>(null);
const ulRef = React.useRef<HTMLUListElement>(null);

const chatOptions = React.useMemo(() => {
return { messageDecoder, messageEncoder };
}, [messageDecoder, messageEncoder]);
const chatOptions: ChatOptions = React.useMemo(() => {
return { messageDecoder, messageEncoder, channelTopic };
}, [messageDecoder, messageEncoder, channelTopic]);

const { send, chatMessages, isSending } = useChat(chatOptions);

Expand Down

0 comments on commit adc3d04

Please sign in to comment.