Skip to content

Commit 22fa65e

Browse files
authored
Add support for new chat API (#979)
1 parent 95ab29a commit 22fa65e

File tree

14 files changed

+230
-129
lines changed

14 files changed

+230
-129
lines changed

.changeset/rotten-bikes-burn.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@livekit/components-core": patch
3+
"@livekit/components-react": patch
4+
---
5+
6+
Add support for new chat API

.npmrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dedupe-peer-dependents=true
2+
resolve-peers-from-workspace-root=true
3+
manage-package-manager-versions=true
4+
package-manager-strict-version=true

docs/storybook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"dependencies": {
1414
"@livekit/components-react": "workspace:*",
1515
"@livekit/components-styles": "workspace:*",
16-
"livekit-client": "^2.4.0",
16+
"livekit-client": "^2.5.4",
1717
"react": "^18.2.0",
1818
"react-dom": "^18.2.0"
1919
},

examples/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"@livekit/components-react": "workspace:*",
1313
"@livekit/components-styles": "workspace:*",
1414
"@livekit/track-processors": "^0.3.2",
15-
"livekit-client": "^2.4.0",
15+
"livekit-client": "^2.5.4",
1616
"livekit-server-sdk": "^2.6.1",
1717
"next": "^12.3.4",
1818
"react": "^18.2.0",

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@
4040
"turbo": "^2.1.1",
4141
"typescript": "5.4.2"
4242
},
43+
"dependencies": {
44+
"livekit-client": "^2.5.4"
45+
},
4346
"engines": {
4447
"node": ">=18"
4548
},
46-
"packageManager": "pnpm@9.2.0"
49+
"packageManager": "pnpm@9.10.0"
4750
}

packages/core/etc/components-core.api.md

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77
import type { AudioCaptureOptions } from 'livekit-client';
88
import { BehaviorSubject } from 'rxjs';
9+
import { ChatMessage } from 'livekit-client';
910
import { ConnectionQuality } from 'livekit-client';
1011
import { ConnectionState } from 'livekit-client';
1112
import { DataPacket_Kind } from 'livekit-client';
1213
import type { DataPublishOptions } from 'livekit-client';
1314
import { LocalAudioTrack } from 'livekit-client';
14-
import type { LocalParticipant } from 'livekit-client';
15+
import { LocalParticipant } from 'livekit-client';
1516
import { LocalVideoTrack } from 'livekit-client';
1617
import loglevel from 'loglevel';
1718
import { Observable } from 'rxjs';
@@ -72,20 +73,12 @@ export interface BaseDataMessage<T extends string | undefined> {
7273
// @public (undocumented)
7374
export type CaptureOptionsBySource<T extends ToggleSource> = T extends Track.Source.Camera ? VideoCaptureOptions : T extends Track.Source.Microphone ? AudioCaptureOptions : T extends Track.Source.ScreenShare ? ScreenShareCaptureOptions : never;
7475

75-
// @public (undocumented)
76-
export interface ChatMessage {
77-
// (undocumented)
78-
id: string;
79-
// (undocumented)
80-
message: string;
81-
// (undocumented)
82-
timestamp: number;
83-
}
76+
export { ChatMessage }
8477

8578
// @public (undocumented)
8679
export type ChatOptions = {
87-
messageEncoder?: (message: ChatMessage) => Uint8Array;
88-
messageDecoder?: (message: Uint8Array) => ReceivedChatMessage;
80+
messageEncoder?: (message: LegacyChatMessage) => Uint8Array;
81+
messageDecoder?: (message: Uint8Array) => LegacyReceivedChatMessage;
8982
channelTopic?: string;
9083
updateChannelTopic?: string;
9184
};
@@ -115,6 +108,9 @@ export function connectionStateObserver(room: Room): Observable<ConnectionState>
115108
// @public (undocumented)
116109
export function createActiveDeviceObservable(room: Room, kind: MediaDeviceKind): Observable<string | undefined>;
117110

111+
// @public (undocumented)
112+
export function createChatObserver(room: Room): Observable<[message: ChatMessage, participant?: LocalParticipant | RemoteParticipant | undefined]>;
113+
118114
// @public (undocumented)
119115
export function createConnectionQualityObserver(participant: Participant): Observable<ConnectionQuality>;
120116

@@ -263,6 +259,18 @@ export function isTrackReferencePlaceholder(trackReference?: TrackReferenceOrPla
263259
// @internal (undocumented)
264260
export function isWeb(): boolean;
265261

262+
// @public (undocumented)
263+
export interface LegacyChatMessage extends ChatMessage {
264+
// (undocumented)
265+
ignore?: true;
266+
}
267+
268+
// @public (undocumented)
269+
export interface LegacyReceivedChatMessage extends ReceivedChatMessage {
270+
// (undocumented)
271+
ignore?: true;
272+
}
273+
266274
// @alpha
267275
export function loadUserChoices(defaults?: Partial<LocalUserChoices>,
268276
preventLoad?: boolean): LocalUserChoices;
@@ -287,11 +295,11 @@ export type MediaToggleType<T extends ToggleSource> = {
287295
enabledObserver: Observable<boolean>;
288296
};
289297

290-
// @public (undocumented)
291-
export type MessageDecoder = (message: Uint8Array) => ReceivedChatMessage;
298+
// @public @deprecated (undocumented)
299+
export type MessageDecoder = (message: Uint8Array) => LegacyReceivedChatMessage;
292300

293-
// @public (undocumented)
294-
export type MessageEncoder = (message: ChatMessage) => Uint8Array;
301+
// @public @deprecated (undocumented)
302+
export type MessageEncoder = (message: LegacyChatMessage) => Uint8Array;
295303

296304
// @public (undocumented)
297305
export function mutedObserver(trackRef: TrackReferenceOrPlaceholder): Observable<boolean>;
@@ -400,8 +408,6 @@ export type PinState = TrackReferenceOrPlaceholder[];
400408

401409
// @public (undocumented)
402410
export interface ReceivedChatMessage extends ChatMessage {
403-
// (undocumented)
404-
editTimestamp?: number;
405411
// (undocumented)
406412
from?: Participant;
407413
}
@@ -492,7 +498,24 @@ export function setupChat(room: Room, options?: ChatOptions): {
492498
messageObservable: Observable<ReceivedChatMessage[]>;
493499
isSendingObservable: BehaviorSubject<boolean>;
494500
send: (message: string) => Promise<ChatMessage>;
495-
update: (message: string, messageId: string) => Promise<ChatMessage>;
501+
update: (message: string, originalMessageOrId: string | ChatMessage) => Promise<{
502+
readonly message: string;
503+
readonly editTimestamp: number;
504+
readonly id: string;
505+
readonly timestamp: number;
506+
}>;
507+
};
508+
509+
// @public (undocumented)
510+
export function setupChatMessageHandler(room: Room): {
511+
chatObservable: Observable<[message: ChatMessage, participant?: LocalParticipant | RemoteParticipant | undefined]>;
512+
send: (text: string) => Promise<ChatMessage>;
513+
edit: (text: string, originalMsg: ChatMessage) => Promise<{
514+
readonly message: string;
515+
readonly editTimestamp: number;
516+
readonly id: string;
517+
readonly timestamp: number;
518+
}>;
496519
};
497520

498521
// @public (undocumented)

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
"rxjs": "7.8.1"
4141
},
4242
"peerDependencies": {
43-
"livekit-client": "^2.4.0",
44-
"@livekit/protocol": "^1.20.1",
43+
"livekit-client": "^2.5.4",
4544
"tslib": "^2.6.2"
4645
},
4746
"devDependencies": {
4847
"@livekit/components-styles": "workspace:*",
48+
"@livekit/protocol": "^1.22.0",
4949
"@microsoft/api-extractor": "^7.36.0",
5050
"@size-limit/file": "^11.0.2",
5151
"@size-limit/webpack": "^11.0.2",

packages/core/src/components/chat.ts

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
11
/* eslint-disable camelcase */
2-
import type { Participant, Room } from 'livekit-client';
2+
import type { Participant, Room, ChatMessage } from 'livekit-client';
33
import { RoomEvent } from 'livekit-client';
4-
import { BehaviorSubject, Subject, scan, map, takeUntil } from 'rxjs';
5-
import { DataTopic, sendMessage, setupDataMessageHandler } from '../observables/dataChannel';
4+
import { BehaviorSubject, Subject, scan, map, takeUntil, merge } from 'rxjs';
5+
import {
6+
DataTopic,
7+
sendMessage,
8+
setupChatMessageHandler,
9+
setupDataMessageHandler,
10+
} from '../observables/dataChannel';
611

712
/** @public */
8-
export interface ChatMessage {
9-
id: string;
10-
timestamp: number;
11-
message: string;
12-
}
13+
export type { ChatMessage };
1314

1415
/** @public */
1516
export interface ReceivedChatMessage extends ChatMessage {
1617
from?: Participant;
17-
editTimestamp?: number;
1818
}
1919

20-
/** @public */
21-
export type MessageEncoder = (message: ChatMessage) => Uint8Array;
22-
/** @public */
23-
export type MessageDecoder = (message: Uint8Array) => ReceivedChatMessage;
20+
export interface LegacyChatMessage extends ChatMessage {
21+
ignore?: true;
22+
}
23+
24+
export interface LegacyReceivedChatMessage extends ReceivedChatMessage {
25+
ignore?: true;
26+
}
27+
28+
/**
29+
* @public
30+
* @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead
31+
*/
32+
export type MessageEncoder = (message: LegacyChatMessage) => Uint8Array;
33+
/**
34+
* @public
35+
* @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead
36+
*/
37+
export type MessageDecoder = (message: Uint8Array) => LegacyReceivedChatMessage;
2438
/** @public */
2539
export type ChatOptions = {
26-
messageEncoder?: (message: ChatMessage) => Uint8Array;
27-
messageDecoder?: (message: Uint8Array) => ReceivedChatMessage;
40+
/** @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead */
41+
messageEncoder?: (message: LegacyChatMessage) => Uint8Array;
42+
/** @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead */
43+
messageDecoder?: (message: Uint8Array) => LegacyReceivedChatMessage;
44+
/** @deprecated the new chat API doesn't rely on topics anymore and uses a dedicated chat API instead */
2845
channelTopic?: string;
46+
/** @deprecated the new chat API doesn't rely on topics anymore and uses a dedicated chat API instead */
2947
updateChannelTopic?: string;
3048
};
3149

@@ -40,9 +58,10 @@ const decoder = new TextDecoder();
4058

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

43-
const encode = (message: ChatMessage) => encoder.encode(JSON.stringify(message));
61+
const encode = (message: LegacyReceivedChatMessage) => encoder.encode(JSON.stringify(message));
4462

45-
const decode = (message: Uint8Array) => JSON.parse(decoder.decode(message)) as ReceivedChatMessage;
63+
const decode = (message: Uint8Array) =>
64+
JSON.parse(decoder.decode(message)) as LegacyReceivedChatMessage | ReceivedChatMessage;
4665

4766
export function setupChat(room: Room, options?: ChatOptions) {
4867
const onDestroyObservable = new Subject<void>();
@@ -67,17 +86,33 @@ export function setupChat(room: Room, options?: ChatOptions) {
6786
const { messageObservable } = setupDataMessageHandler(room, [topic, updateTopic]);
6887
messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject);
6988
}
89+
const { chatObservable, send: sendChatMessage } = setupChatMessageHandler(room);
7090

7191
const finalMessageDecoder = messageDecoder ?? decode;
7292

7393
/** Build up the message array over time. */
74-
const messagesObservable = messageSubject.pipe(
75-
map((msg) => {
76-
const parsedMessage = finalMessageDecoder(msg.payload);
77-
const newMessage: ReceivedChatMessage = { ...parsedMessage, from: msg.from };
78-
return newMessage;
79-
}),
80-
scan<ReceivedChatMessage, ReceivedChatMessage[]>((acc, value) => {
94+
const messagesObservable = merge(
95+
messageSubject.pipe(
96+
map((msg) => {
97+
const parsedMessage = finalMessageDecoder(msg.payload);
98+
const newMessage = { ...parsedMessage, from: msg.from };
99+
if (isIgnorableChatMessage(newMessage)) {
100+
return undefined;
101+
}
102+
return newMessage;
103+
}),
104+
),
105+
chatObservable.pipe(
106+
map(([msg, participant]) => {
107+
return { ...msg, from: participant };
108+
}),
109+
),
110+
).pipe(
111+
scan<ReceivedChatMessage | undefined, ReceivedChatMessage[]>((acc, value) => {
112+
// ignore legacy message updates
113+
if (!value) {
114+
return acc;
115+
}
81116
// handle message updates
82117
if (
83118
'id' in value &&
@@ -89,7 +124,7 @@ export function setupChat(room: Room, options?: ChatOptions) {
89124
acc[replaceIndex] = {
90125
...value,
91126
timestamp: originalMsg.timestamp,
92-
editTimestamp: value.timestamp,
127+
editTimestamp: value.editTimestamp ?? value.timestamp,
93128
};
94129
}
95130

@@ -105,43 +140,35 @@ export function setupChat(room: Room, options?: ChatOptions) {
105140
const finalMessageEncoder = messageEncoder ?? encode;
106141

107142
const send = async (message: string) => {
108-
const timestamp = Date.now();
109-
const id = crypto.randomUUID();
110-
const chatMessage: ChatMessage = { id, message, timestamp };
111-
const encodedMsg = finalMessageEncoder(chatMessage);
112143
isSending$.next(true);
113144
try {
114-
await sendMessage(room.localParticipant, encodedMsg, {
145+
const chatMessage = await sendChatMessage(message);
146+
const encodedLegacyMsg = finalMessageEncoder({ ...chatMessage, ignore: true });
147+
await sendMessage(room.localParticipant, encodedLegacyMsg, {
115148
reliable: true,
116149
topic,
117150
});
118-
messageSubject.next({
119-
payload: encodedMsg,
120-
topic: topic,
121-
from: room.localParticipant,
122-
});
123151
return chatMessage;
124152
} finally {
125153
isSending$.next(false);
126154
}
127155
};
128156

129-
const update = async (message: string, messageId: string) => {
157+
const update = async (message: string, originalMessageOrId: string | ChatMessage) => {
130158
const timestamp = Date.now();
131-
const chatMessage: ChatMessage = { id: messageId, message, timestamp };
132-
const encodedMsg = finalMessageEncoder(chatMessage);
159+
const originalMessage: ChatMessage =
160+
typeof originalMessageOrId === 'string'
161+
? { id: originalMessageOrId, message: '', timestamp }
162+
: originalMessageOrId;
133163
isSending$.next(true);
134164
try {
135-
await sendMessage(room.localParticipant, encodedMsg, {
165+
const editedMessage = await room.localParticipant.editChatMessage(message, originalMessage);
166+
const encodedLegacyMessage = finalMessageEncoder(editedMessage);
167+
await sendMessage(room.localParticipant, encodedLegacyMessage, {
136168
topic: updateTopic,
137169
reliable: true,
138170
});
139-
messageSubject.next({
140-
payload: encodedMsg,
141-
topic: topic,
142-
from: room.localParticipant,
143-
});
144-
return chatMessage;
171+
return editedMessage;
145172
} finally {
146173
isSending$.next(false);
147174
}
@@ -154,5 +181,16 @@ export function setupChat(room: Room, options?: ChatOptions) {
154181
}
155182
room.once(RoomEvent.Disconnected, destroy);
156183

157-
return { messageObservable: messagesObservable, isSendingObservable: isSending$, send, update };
184+
return {
185+
messageObservable: messagesObservable,
186+
isSendingObservable: isSending$,
187+
send,
188+
update,
189+
};
190+
}
191+
192+
function isIgnorableChatMessage(
193+
msg: ReceivedChatMessage | LegacyReceivedChatMessage,
194+
): msg is ReceivedChatMessage {
195+
return (msg as LegacyChatMessage).ignore == true;
158196
}

0 commit comments

Comments
 (0)