@meshagent/meshagent-tailwind 0.39.9 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cjs/{ChatBotView.js → chat/chat-bot-view.js} +37 -22
  3. package/dist/cjs/{chat-hooks.d.ts → chat/chat-hooks.d.ts} +5 -1
  4. package/dist/cjs/{chat-hooks.js → chat/chat-hooks.js} +12 -2
  5. package/dist/cjs/{ChatInput.js → chat/chat-input.js} +9 -9
  6. package/dist/cjs/chat/chat-thread.d.ts +12 -0
  7. package/dist/cjs/{ChatThread.js → chat/chat-thread.js} +75 -28
  8. package/dist/cjs/{ChatTypingIndicator.js → chat/chat-typing-indicator.js} +4 -4
  9. package/dist/cjs/chat/dataset-chat-thread.d.ts +13 -0
  10. package/dist/cjs/chat/dataset-chat-thread.js +1840 -0
  11. package/dist/cjs/{FileUploader.js → chat/file-uploader.js} +4 -4
  12. package/dist/cjs/{multi-thread-view.js → chat/multi-thread-view.js} +8 -3
  13. package/dist/cjs/chat/new-chat-thread.d.ts +17 -0
  14. package/dist/cjs/{Chat.js → chat/new-chat-thread.js} +43 -168
  15. package/dist/cjs/{UploadPill.js → chat/upload-pill.js} +5 -5
  16. package/dist/cjs/file-preview/file-preview.d.ts +34 -0
  17. package/dist/cjs/file-preview/file-preview.js +329 -0
  18. package/dist/cjs/forms/email-address.d.ts +10 -0
  19. package/dist/cjs/forms/email-address.js +105 -0
  20. package/dist/cjs/forms/form.d.ts +27 -0
  21. package/dist/cjs/forms/form.js +200 -0
  22. package/dist/cjs/forms/multi-select-autocomplete.d.ts +35 -0
  23. package/dist/cjs/forms/multi-select-autocomplete.js +294 -0
  24. package/dist/cjs/forms/select-users-dialog.d.ts +20 -0
  25. package/dist/cjs/forms/select-users-dialog.js +145 -0
  26. package/dist/cjs/forms/select-users.d.ts +16 -0
  27. package/dist/cjs/forms/select-users.js +117 -0
  28. package/dist/cjs/index.d.ts +19 -11
  29. package/dist/cjs/index.js +19 -11
  30. package/dist/cjs/meetings/audio-visualization.d.ts +7 -0
  31. package/dist/cjs/meetings/audio-visualization.js +74 -0
  32. package/dist/cjs/meetings/controls.d.ts +19 -0
  33. package/dist/cjs/meetings/controls.js +300 -0
  34. package/dist/cjs/meetings/meeting-scope.d.ts +83 -0
  35. package/dist/cjs/meetings/meeting-scope.js +309 -0
  36. package/dist/cjs/meetings/meetings.d.ts +5 -0
  37. package/dist/cjs/meetings/meetings.js +22 -0
  38. package/dist/cjs/meetings/participants.d.ts +13 -0
  39. package/dist/cjs/meetings/participants.js +154 -0
  40. package/dist/cjs/meetings/wake-lock.d.ts +4 -0
  41. package/dist/cjs/meetings/wake-lock.js +55 -0
  42. package/dist/esm/{ChatBotView.js → chat/chat-bot-view.js} +34 -19
  43. package/dist/esm/{chat-hooks.d.ts → chat/chat-hooks.d.ts} +5 -1
  44. package/dist/esm/{chat-hooks.js → chat/chat-hooks.js} +12 -2
  45. package/dist/esm/{ChatInput.js → chat/chat-input.js} +4 -4
  46. package/dist/esm/chat/chat-thread.d.ts +12 -0
  47. package/dist/esm/{ChatThread.js → chat/chat-thread.js} +70 -23
  48. package/dist/esm/{ChatTypingIndicator.js → chat/chat-typing-indicator.js} +1 -1
  49. package/dist/esm/chat/dataset-chat-thread.d.ts +13 -0
  50. package/dist/esm/chat/dataset-chat-thread.js +1815 -0
  51. package/dist/esm/{FileUploader.js → chat/file-uploader.js} +1 -1
  52. package/dist/esm/{multi-thread-view.js → chat/multi-thread-view.js} +8 -3
  53. package/dist/esm/chat/new-chat-thread.d.ts +17 -0
  54. package/dist/esm/{Chat.js → chat/new-chat-thread.js} +40 -165
  55. package/dist/esm/{UploadPill.js → chat/upload-pill.js} +2 -2
  56. package/dist/esm/file-preview/file-preview.d.ts +34 -0
  57. package/dist/esm/file-preview/file-preview.js +316 -0
  58. package/dist/esm/forms/email-address.d.ts +10 -0
  59. package/dist/esm/forms/email-address.js +85 -0
  60. package/dist/esm/forms/form.d.ts +27 -0
  61. package/dist/esm/forms/form.js +193 -0
  62. package/dist/esm/forms/multi-select-autocomplete.d.ts +35 -0
  63. package/dist/esm/forms/multi-select-autocomplete.js +274 -0
  64. package/dist/esm/forms/select-users-dialog.d.ts +20 -0
  65. package/dist/esm/forms/select-users-dialog.js +132 -0
  66. package/dist/esm/forms/select-users.d.ts +16 -0
  67. package/dist/esm/forms/select-users.js +97 -0
  68. package/dist/esm/index.d.ts +19 -11
  69. package/dist/esm/index.js +19 -11
  70. package/dist/esm/meetings/audio-visualization.d.ts +7 -0
  71. package/dist/esm/meetings/audio-visualization.js +54 -0
  72. package/dist/esm/meetings/controls.d.ts +19 -0
  73. package/dist/esm/meetings/controls.js +294 -0
  74. package/dist/esm/meetings/meeting-scope.d.ts +83 -0
  75. package/dist/esm/meetings/meeting-scope.js +294 -0
  76. package/dist/esm/meetings/meetings.d.ts +5 -0
  77. package/dist/esm/meetings/meetings.js +5 -0
  78. package/dist/esm/meetings/participants.d.ts +13 -0
  79. package/dist/esm/meetings/participants.js +137 -0
  80. package/dist/esm/meetings/wake-lock.d.ts +4 -0
  81. package/dist/esm/meetings/wake-lock.js +35 -0
  82. package/dist/index.css +2 -2
  83. package/package.json +7 -4
  84. package/dist/cjs/Chat.d.ts +0 -15
  85. package/dist/cjs/ChatThread.d.ts +0 -21
  86. package/dist/esm/Chat.d.ts +0 -15
  87. package/dist/esm/ChatThread.d.ts +0 -21
  88. /package/dist/cjs/{ChatBotView.d.ts → chat/chat-bot-view.d.ts} +0 -0
  89. /package/dist/cjs/{ChatInput.d.ts → chat/chat-input.d.ts} +0 -0
  90. /package/dist/cjs/{chat-message.d.ts → chat/chat-message.d.ts} +0 -0
  91. /package/dist/cjs/{chat-message.js → chat/chat-message.js} +0 -0
  92. /package/dist/cjs/{ChatTypingIndicator.d.ts → chat/chat-typing-indicator.d.ts} +0 -0
  93. /package/dist/cjs/{conversation-descriptor.d.ts → chat/conversation-descriptor.d.ts} +0 -0
  94. /package/dist/cjs/{conversation-descriptor.js → chat/conversation-descriptor.js} +0 -0
  95. /package/dist/cjs/{file-attachment.d.ts → chat/file-attachment.d.ts} +0 -0
  96. /package/dist/cjs/{file-attachment.js → chat/file-attachment.js} +0 -0
  97. /package/dist/cjs/{FileUploader.d.ts → chat/file-uploader.d.ts} +0 -0
  98. /package/dist/cjs/{multi-thread-view.d.ts → chat/multi-thread-view.d.ts} +0 -0
  99. /package/dist/cjs/{UploadPill.d.ts → chat/upload-pill.d.ts} +0 -0
  100. /package/dist/esm/{ChatBotView.d.ts → chat/chat-bot-view.d.ts} +0 -0
  101. /package/dist/esm/{ChatInput.d.ts → chat/chat-input.d.ts} +0 -0
  102. /package/dist/esm/{chat-message.d.ts → chat/chat-message.d.ts} +0 -0
  103. /package/dist/esm/{chat-message.js → chat/chat-message.js} +0 -0
  104. /package/dist/esm/{ChatTypingIndicator.d.ts → chat/chat-typing-indicator.d.ts} +0 -0
  105. /package/dist/esm/{conversation-descriptor.d.ts → chat/conversation-descriptor.d.ts} +0 -0
  106. /package/dist/esm/{conversation-descriptor.js → chat/conversation-descriptor.js} +0 -0
  107. /package/dist/esm/{file-attachment.d.ts → chat/file-attachment.d.ts} +0 -0
  108. /package/dist/esm/{file-attachment.js → chat/file-attachment.js} +0 -0
  109. /package/dist/esm/{FileUploader.d.ts → chat/file-uploader.d.ts} +0 -0
  110. /package/dist/esm/{multi-thread-view.d.ts → chat/multi-thread-view.d.ts} +0 -0
  111. /package/dist/esm/{UploadPill.d.ts → chat/upload-pill.d.ts} +0 -0
@@ -0,0 +1,294 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
3
+ import { Camera, CameraOff, Mic, MicOff, Phone, Settings } from "lucide-react";
4
+ import { ConnectionState, Room } from "livekit-client";
5
+ import { Button } from "../components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger
14
+ } from "../components/ui/dialog";
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue
21
+ } from "../components/ui/select";
22
+ import { Spinner } from "../components/ui/spinner";
23
+ import { cn } from "../lib/utils";
24
+ import { useMeetingController } from "./meeting-scope";
25
+ function useControllerVersion(controller) {
26
+ useSyncExternalStore(
27
+ (listener) => controller.subscribe(listener),
28
+ () => controller.livekitRoom.state,
29
+ () => controller.livekitRoom.state
30
+ );
31
+ useSyncExternalStore(
32
+ (listener) => controller.pendingLocalMedia.subscribe(listener),
33
+ () => `${controller.pendingLocalMedia.cameraPending}:${controller.pendingLocalMedia.microphonePending}:${controller.pendingLocalMedia.cameraUnavailable}:${controller.pendingLocalMedia.microphoneUnavailable}`,
34
+ () => ""
35
+ );
36
+ }
37
+ function MeetingControls({
38
+ controller: providedController,
39
+ spacing = 5
40
+ }) {
41
+ const controller = useMeetingController(providedController);
42
+ useControllerVersion(controller);
43
+ const hasLocalParticipant = controller.livekitRoom.localParticipant != null;
44
+ return /* @__PURE__ */ jsxs(
45
+ "div",
46
+ {
47
+ className: "flex flex-wrap items-center justify-center",
48
+ style: { gap: spacing },
49
+ children: [
50
+ /* @__PURE__ */ jsx(ConnectionButton, { controller }),
51
+ hasLocalParticipant ? /* @__PURE__ */ jsxs(Fragment, { children: [
52
+ /* @__PURE__ */ jsx(MicToggle, { controller }),
53
+ /* @__PURE__ */ jsx(CameraToggle, { controller }),
54
+ /* @__PURE__ */ jsx(ChangeSettings, { room: controller.livekitRoom })
55
+ ] }) : null
56
+ ]
57
+ }
58
+ );
59
+ }
60
+ function describeCameraToggleError(error) {
61
+ const message = String(error);
62
+ if (message.includes("NotAllowedError")) {
63
+ return "Camera access was blocked by the browser or system.";
64
+ }
65
+ if (message.includes("NotFoundError")) {
66
+ return "The selected camera was not found.";
67
+ }
68
+ return `Unable to change camera state: ${message}`;
69
+ }
70
+ function describeMicrophoneToggleError(error) {
71
+ const message = String(error);
72
+ if (message.includes("NotAllowedError")) {
73
+ return "Microphone access was blocked by the browser or system.";
74
+ }
75
+ if (message.includes("NotFoundError")) {
76
+ return "The selected microphone was not found.";
77
+ }
78
+ return `Unable to change microphone state: ${message}`;
79
+ }
80
+ function useDeviceAvailable(kind) {
81
+ const [available, setAvailable] = useState(true);
82
+ useEffect(() => {
83
+ let cancelled = false;
84
+ const update = async () => {
85
+ const devices = await navigator.mediaDevices?.enumerateDevices?.();
86
+ if (!cancelled && devices != null) {
87
+ setAvailable(devices.some((device) => device.kind === kind && device.deviceId !== ""));
88
+ }
89
+ };
90
+ void update().catch(() => setAvailable(false));
91
+ navigator.mediaDevices?.addEventListener?.("devicechange", update);
92
+ return () => {
93
+ cancelled = true;
94
+ navigator.mediaDevices?.removeEventListener?.("devicechange", update);
95
+ };
96
+ }, [kind]);
97
+ return available;
98
+ }
99
+ function CameraToggle({ controller }) {
100
+ const resolvedController = useMeetingController(controller);
101
+ useControllerVersion(resolvedController);
102
+ const [processing, setProcessing] = useState(false);
103
+ const deviceAvailable = useDeviceAvailable("videoinput");
104
+ const localParticipant = resolvedController.livekitRoom.localParticipant;
105
+ if (localParticipant == null) {
106
+ return null;
107
+ }
108
+ const enabled = localParticipant.isCameraEnabled;
109
+ const pending = resolvedController.pendingLocalMedia.cameraPending;
110
+ const showEnabled = enabled || pending;
111
+ const unavailable = (resolvedController.pendingLocalMedia.cameraUnavailable || !deviceAvailable) && !showEnabled;
112
+ return /* @__PURE__ */ jsx(
113
+ MeetingControlsButton,
114
+ {
115
+ text: pending ? "Starting camera" : enabled ? "Turn off camera" : "Turn on camera",
116
+ on: showEnabled,
117
+ destructive: unavailable,
118
+ icon: showEnabled ? /* @__PURE__ */ jsx(Camera, {}) : /* @__PURE__ */ jsx(CameraOff, {}),
119
+ loading: pending || processing,
120
+ disabled: processing || pending,
121
+ onClick: () => {
122
+ if (processing || pending) {
123
+ return;
124
+ }
125
+ setProcessing(true);
126
+ void localParticipant.setCameraEnabled(!enabled).then(() => resolvedController.pendingLocalMedia.setCameraUnavailable(false)).catch((error) => {
127
+ resolvedController.pendingLocalMedia.setCameraUnavailable(true);
128
+ console.warn(describeCameraToggleError(error));
129
+ }).finally(() => setProcessing(false));
130
+ }
131
+ }
132
+ );
133
+ }
134
+ function MicToggle({ controller }) {
135
+ const resolvedController = useMeetingController(controller);
136
+ useControllerVersion(resolvedController);
137
+ const [processing, setProcessing] = useState(false);
138
+ const deviceAvailable = useDeviceAvailable("audioinput");
139
+ const localParticipant = resolvedController.livekitRoom.localParticipant;
140
+ if (localParticipant == null) {
141
+ return null;
142
+ }
143
+ const enabled = localParticipant.isMicrophoneEnabled;
144
+ const pending = resolvedController.pendingLocalMedia.microphonePending;
145
+ const showEnabled = enabled || pending;
146
+ const unavailable = (resolvedController.pendingLocalMedia.microphoneUnavailable || !deviceAvailable) && !showEnabled;
147
+ return /* @__PURE__ */ jsx(
148
+ MeetingControlsButton,
149
+ {
150
+ text: pending ? "Starting mic" : enabled ? "Turn off mic" : "Turn on mic",
151
+ on: showEnabled,
152
+ destructive: unavailable,
153
+ icon: showEnabled ? /* @__PURE__ */ jsx(Mic, {}) : /* @__PURE__ */ jsx(MicOff, {}),
154
+ loading: pending || processing,
155
+ disabled: processing || pending,
156
+ onClick: () => {
157
+ if (processing || pending) {
158
+ return;
159
+ }
160
+ setProcessing(true);
161
+ void localParticipant.setMicrophoneEnabled(!enabled).then(() => resolvedController.pendingLocalMedia.setMicrophoneUnavailable(false)).catch((error) => {
162
+ resolvedController.pendingLocalMedia.setMicrophoneUnavailable(true);
163
+ console.warn(describeMicrophoneToggleError(error));
164
+ }).finally(() => setProcessing(false));
165
+ }
166
+ }
167
+ );
168
+ }
169
+ function ConnectionButton({ controller }) {
170
+ const resolvedController = useMeetingController(controller);
171
+ useControllerVersion(resolvedController);
172
+ const state = resolvedController.livekitRoom.state;
173
+ if (state === ConnectionState.Connected) {
174
+ return /* @__PURE__ */ jsx(
175
+ MeetingControlsButton,
176
+ {
177
+ text: "Hangup",
178
+ destructive: true,
179
+ icon: /* @__PURE__ */ jsx(Phone, {}),
180
+ onClick: () => {
181
+ void resolvedController.disconnect();
182
+ }
183
+ }
184
+ );
185
+ }
186
+ if (state === ConnectionState.Disconnected) {
187
+ return /* @__PURE__ */ jsx(
188
+ MeetingControlsButton,
189
+ {
190
+ text: "Connect",
191
+ icon: /* @__PURE__ */ jsx(Phone, {}),
192
+ onClick: () => {
193
+ void resolvedController.connect();
194
+ }
195
+ }
196
+ );
197
+ }
198
+ return /* @__PURE__ */ jsx(MeetingControlsButton, { text: "Connecting", icon: /* @__PURE__ */ jsx(Phone, {}), loading: true, disabled: true });
199
+ }
200
+ function MeetingControlsButton({
201
+ text,
202
+ icon,
203
+ on,
204
+ destructive,
205
+ loading,
206
+ disabled,
207
+ onClick
208
+ }) {
209
+ return /* @__PURE__ */ jsx(
210
+ Button,
211
+ {
212
+ type: "button",
213
+ title: text,
214
+ "aria-label": text,
215
+ size: "icon",
216
+ variant: destructive ? "destructive" : on ? "default" : "outline",
217
+ disabled,
218
+ onClick,
219
+ className: cn("h-12 w-12", on && !destructive ? "bg-emerald-600 text-white hover:bg-emerald-700" : null),
220
+ children: loading ? /* @__PURE__ */ jsx(Spinner, { className: "h-5 w-5" }) : icon
221
+ }
222
+ );
223
+ }
224
+ function deviceLabel(device, fallbackPrefix) {
225
+ const label = device?.label.trim();
226
+ return label != null && label !== "" ? label.replace(/^Default - /u, "") : `Default ${fallbackPrefix}`;
227
+ }
228
+ function devicesForKind(devices, kind) {
229
+ return devices.filter((device) => device.kind === kind && device.deviceId !== "");
230
+ }
231
+ function DeviceSelect({
232
+ label,
233
+ devices,
234
+ kind,
235
+ room
236
+ }) {
237
+ const options = devicesForKind(devices, kind);
238
+ const activeDevice = room.getActiveDevice(kind) ?? options[0]?.deviceId ?? "";
239
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
240
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: label }),
241
+ /* @__PURE__ */ jsxs(
242
+ Select,
243
+ {
244
+ value: activeDevice,
245
+ onValueChange: (deviceId) => {
246
+ void room.switchActiveDevice(kind, deviceId).catch((error) => {
247
+ console.warn(`Unable to switch ${label.toLowerCase()}`, error);
248
+ });
249
+ },
250
+ children: [
251
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: deviceLabel(options[0] ?? null, label) }) }),
252
+ /* @__PURE__ */ jsx(SelectContent, { children: options.map((device) => /* @__PURE__ */ jsx(SelectItem, { value: device.deviceId, children: deviceLabel(device, label) }, device.deviceId)) })
253
+ ]
254
+ }
255
+ )
256
+ ] });
257
+ }
258
+ function ChangeSettings({ room }) {
259
+ const [devices, setDevices] = useState([]);
260
+ const refreshDevices = useCallback(() => {
261
+ void Room.getLocalDevices(void 0, false).then(setDevices).catch(() => setDevices([]));
262
+ }, []);
263
+ useEffect(() => {
264
+ refreshDevices();
265
+ navigator.mediaDevices?.addEventListener?.("devicechange", refreshDevices);
266
+ return () => navigator.mediaDevices?.removeEventListener?.("devicechange", refreshDevices);
267
+ }, [refreshDevices]);
268
+ const hasAudioOutput = useMemo(
269
+ () => devices.some((device) => device.kind === "audiooutput"),
270
+ [devices]
271
+ );
272
+ return /* @__PURE__ */ jsxs(Dialog, { children: [
273
+ /* @__PURE__ */ jsx(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", title: "Change settings", "aria-label": "Change settings", variant: "outline", size: "icon", className: "h-12 w-12", children: /* @__PURE__ */ jsx(Settings, {}) }) }),
274
+ /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-[min(92vw,560px)]", children: [
275
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
276
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Meeting settings" }),
277
+ /* @__PURE__ */ jsx(DialogDescription, { children: "Choose the devices used for this meeting." })
278
+ ] }),
279
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
280
+ /* @__PURE__ */ jsx(DeviceSelect, { label: "Camera", devices, kind: "videoinput", room }),
281
+ /* @__PURE__ */ jsx(DeviceSelect, { label: "Microphone", devices, kind: "audioinput", room }),
282
+ hasAudioOutput ? /* @__PURE__ */ jsx(DeviceSelect, { label: "Speaker", devices, kind: "audiooutput", room }) : null
283
+ ] }),
284
+ /* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: refreshDevices, children: "Refresh" }) })
285
+ ] })
286
+ ] });
287
+ }
288
+ export {
289
+ CameraToggle,
290
+ ChangeSettings,
291
+ ConnectionButton,
292
+ MeetingControls,
293
+ MicToggle
294
+ };
@@ -0,0 +1,83 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { RoomClient } from "@meshagent/meshagent";
3
+ import { Room, type LocalParticipant, type Participant, type RoomConnectOptions, type RoomOptions, type TrackPublication } from "livekit-client";
4
+ type Listener = () => void;
5
+ export interface LivekitConnectionInfo {
6
+ url: string;
7
+ token: string;
8
+ }
9
+ export type MeetingFastConnectOptions = RoomConnectOptions & {
10
+ camera?: {
11
+ enabled?: boolean;
12
+ };
13
+ microphone?: {
14
+ enabled?: boolean;
15
+ };
16
+ };
17
+ export declare class PendingLocalMediaState {
18
+ private listeners;
19
+ private cameraAwaitingEnableConfirmation;
20
+ private microphoneAwaitingEnableConfirmation;
21
+ private _cameraPending;
22
+ private _microphonePending;
23
+ private _cameraUnavailable;
24
+ private _microphoneUnavailable;
25
+ get cameraPending(): boolean;
26
+ get microphonePending(): boolean;
27
+ get cameraUnavailable(): boolean;
28
+ get microphoneUnavailable(): boolean;
29
+ subscribe(listener: Listener): () => void;
30
+ setCameraPending(value: boolean, { awaitEnableConfirmation }?: {
31
+ awaitEnableConfirmation?: boolean | undefined;
32
+ }): void;
33
+ setMicrophonePending(value: boolean, { awaitEnableConfirmation }?: {
34
+ awaitEnableConfirmation?: boolean | undefined;
35
+ }): void;
36
+ setCameraUnavailable(value: boolean): void;
37
+ setMicrophoneUnavailable(value: boolean): void;
38
+ setPending({ cameraPending, microphonePending, cameraAwaitEnableConfirmation, microphoneAwaitEnableConfirmation, }: {
39
+ cameraPending: boolean;
40
+ microphonePending: boolean;
41
+ cameraAwaitEnableConfirmation?: boolean;
42
+ microphoneAwaitEnableConfirmation?: boolean;
43
+ }): void;
44
+ syncFromLocalParticipant(participant: LocalParticipant | undefined, disconnected: boolean): void;
45
+ clear(): void;
46
+ dispose(): void;
47
+ private notify;
48
+ }
49
+ export declare class MeetingController {
50
+ readonly room: RoomClient;
51
+ readonly livekitRoom: Room;
52
+ readonly pendingLocalMedia: PendingLocalMediaState;
53
+ private readonly listeners;
54
+ private _config;
55
+ private _configurationError;
56
+ constructor({ room, roomOptions }: {
57
+ room: RoomClient;
58
+ roomOptions?: RoomOptions;
59
+ });
60
+ get config(): LivekitConnectionInfo | null;
61
+ get configurationError(): unknown;
62
+ get isConnected(): boolean;
63
+ get hasParticipantsWithVideo(): boolean;
64
+ subscribe(listener: Listener): () => void;
65
+ configure({ breakoutRoom }?: {
66
+ breakoutRoom?: string;
67
+ }): Promise<void>;
68
+ connect(options?: MeetingFastConnectOptions): Promise<void>;
69
+ disconnect(): Promise<void>;
70
+ dispose(): void;
71
+ private onRoomChanged;
72
+ private syncPendingLocalMediaState;
73
+ private notify;
74
+ }
75
+ export declare function useMeetingController(controller?: MeetingController): MeetingController;
76
+ export declare function MeetingScope({ client, breakoutRoom, roomOptions, children, }: {
77
+ client: RoomClient;
78
+ breakoutRoom?: string;
79
+ roomOptions?: RoomOptions;
80
+ children: ReactNode | ((controller: MeetingController) => ReactNode);
81
+ }): ReactElement;
82
+ export declare function firstEnabledVideoPublication(participant: Participant): TrackPublication | undefined;
83
+ export {};
@@ -0,0 +1,294 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
3
+ import {
4
+ ConnectionState,
5
+ Room,
6
+ RoomEvent,
7
+ Track
8
+ } from "livekit-client";
9
+ import { WakeLocker } from "./wake-lock";
10
+ class PendingLocalMediaState {
11
+ listeners = /* @__PURE__ */ new Set();
12
+ cameraAwaitingEnableConfirmation = false;
13
+ microphoneAwaitingEnableConfirmation = false;
14
+ _cameraPending = false;
15
+ _microphonePending = false;
16
+ _cameraUnavailable = false;
17
+ _microphoneUnavailable = false;
18
+ get cameraPending() {
19
+ return this._cameraPending;
20
+ }
21
+ get microphonePending() {
22
+ return this._microphonePending;
23
+ }
24
+ get cameraUnavailable() {
25
+ return this._cameraUnavailable;
26
+ }
27
+ get microphoneUnavailable() {
28
+ return this._microphoneUnavailable;
29
+ }
30
+ subscribe(listener) {
31
+ this.listeners.add(listener);
32
+ return () => this.listeners.delete(listener);
33
+ }
34
+ setCameraPending(value, { awaitEnableConfirmation = false } = {}) {
35
+ if (this._cameraPending === value && this.cameraAwaitingEnableConfirmation === awaitEnableConfirmation) {
36
+ return;
37
+ }
38
+ this._cameraPending = value;
39
+ this.cameraAwaitingEnableConfirmation = value && awaitEnableConfirmation;
40
+ this.notify();
41
+ }
42
+ setMicrophonePending(value, { awaitEnableConfirmation = false } = {}) {
43
+ if (this._microphonePending === value && this.microphoneAwaitingEnableConfirmation === awaitEnableConfirmation) {
44
+ return;
45
+ }
46
+ this._microphonePending = value;
47
+ this.microphoneAwaitingEnableConfirmation = value && awaitEnableConfirmation;
48
+ this.notify();
49
+ }
50
+ setCameraUnavailable(value) {
51
+ if (this._cameraUnavailable === value) {
52
+ return;
53
+ }
54
+ this._cameraUnavailable = value;
55
+ this.notify();
56
+ }
57
+ setMicrophoneUnavailable(value) {
58
+ if (this._microphoneUnavailable === value) {
59
+ return;
60
+ }
61
+ this._microphoneUnavailable = value;
62
+ this.notify();
63
+ }
64
+ setPending({
65
+ cameraPending,
66
+ microphonePending,
67
+ cameraAwaitEnableConfirmation = false,
68
+ microphoneAwaitEnableConfirmation = false
69
+ }) {
70
+ const nextCameraAwaiting = cameraPending && cameraAwaitEnableConfirmation;
71
+ const nextMicrophoneAwaiting = microphonePending && microphoneAwaitEnableConfirmation;
72
+ if (this._cameraPending === cameraPending && this._microphonePending === microphonePending && this.cameraAwaitingEnableConfirmation === nextCameraAwaiting && this.microphoneAwaitingEnableConfirmation === nextMicrophoneAwaiting) {
73
+ return;
74
+ }
75
+ this._cameraPending = cameraPending;
76
+ this._microphonePending = microphonePending;
77
+ this.cameraAwaitingEnableConfirmation = nextCameraAwaiting;
78
+ this.microphoneAwaitingEnableConfirmation = nextMicrophoneAwaiting;
79
+ this.notify();
80
+ }
81
+ syncFromLocalParticipant(participant, disconnected) {
82
+ if (disconnected) {
83
+ this.clear();
84
+ return;
85
+ }
86
+ if (this.cameraAwaitingEnableConfirmation && participant?.isCameraEnabled) {
87
+ this.setCameraPending(false);
88
+ }
89
+ if (participant?.isCameraEnabled) {
90
+ this.setCameraUnavailable(false);
91
+ }
92
+ if (this.microphoneAwaitingEnableConfirmation && participant?.isMicrophoneEnabled) {
93
+ this.setMicrophonePending(false);
94
+ }
95
+ if (participant?.isMicrophoneEnabled) {
96
+ this.setMicrophoneUnavailable(false);
97
+ }
98
+ }
99
+ clear() {
100
+ if (!this._cameraPending && !this._microphonePending && !this.cameraAwaitingEnableConfirmation && !this.microphoneAwaitingEnableConfirmation && !this._cameraUnavailable && !this._microphoneUnavailable) {
101
+ return;
102
+ }
103
+ this._cameraPending = false;
104
+ this._microphonePending = false;
105
+ this.cameraAwaitingEnableConfirmation = false;
106
+ this.microphoneAwaitingEnableConfirmation = false;
107
+ this._cameraUnavailable = false;
108
+ this._microphoneUnavailable = false;
109
+ this.notify();
110
+ }
111
+ dispose() {
112
+ this.listeners.clear();
113
+ }
114
+ notify() {
115
+ for (const listener of this.listeners) {
116
+ listener();
117
+ }
118
+ }
119
+ }
120
+ class MeetingController {
121
+ room;
122
+ livekitRoom;
123
+ pendingLocalMedia = new PendingLocalMediaState();
124
+ listeners = /* @__PURE__ */ new Set();
125
+ _config = null;
126
+ _configurationError = null;
127
+ constructor({ room, roomOptions }) {
128
+ this.room = room;
129
+ this.livekitRoom = new Room(roomOptions);
130
+ const roomEvents = [
131
+ RoomEvent.ConnectionStateChanged,
132
+ RoomEvent.Connected,
133
+ RoomEvent.Disconnected,
134
+ RoomEvent.ParticipantConnected,
135
+ RoomEvent.ParticipantDisconnected,
136
+ RoomEvent.TrackPublished,
137
+ RoomEvent.TrackSubscribed,
138
+ RoomEvent.TrackUnpublished,
139
+ RoomEvent.TrackUnsubscribed,
140
+ RoomEvent.TrackMuted,
141
+ RoomEvent.TrackUnmuted,
142
+ RoomEvent.LocalTrackPublished,
143
+ RoomEvent.LocalTrackUnpublished,
144
+ RoomEvent.ActiveSpeakersChanged,
145
+ RoomEvent.ParticipantAttributesChanged,
146
+ RoomEvent.ParticipantNameChanged,
147
+ RoomEvent.MediaDevicesChanged
148
+ ];
149
+ for (const eventName of roomEvents) {
150
+ this.livekitRoom.on(eventName, this.onRoomChanged);
151
+ }
152
+ }
153
+ get config() {
154
+ return this._config;
155
+ }
156
+ get configurationError() {
157
+ return this._configurationError;
158
+ }
159
+ get isConnected() {
160
+ return this.livekitRoom.state !== ConnectionState.Disconnected;
161
+ }
162
+ get hasParticipantsWithVideo() {
163
+ const localHasVideo = Array.from(this.livekitRoom.localParticipant.videoTrackPublications.values()).some((publication) => !publication.isMuted);
164
+ const remoteHasVideo = Array.from(this.livekitRoom.remoteParticipants.values()).some((participant) => Array.from(participant.videoTrackPublications.values()).some((publication) => !publication.isMuted));
165
+ return localHasVideo || remoteHasVideo;
166
+ }
167
+ subscribe(listener) {
168
+ this.listeners.add(listener);
169
+ return () => this.listeners.delete(listener);
170
+ }
171
+ async configure({ breakoutRoom } = {}) {
172
+ if (this.livekitRoom.state !== ConnectionState.Disconnected) {
173
+ throw new Error("You cannot reconfigure while the controller is connected");
174
+ }
175
+ this._config = null;
176
+ this._configurationError = null;
177
+ this.notify();
178
+ try {
179
+ this._config = await this.room.livekit.getConnectionInfo({ breakoutRoom });
180
+ this.notify();
181
+ } catch (error) {
182
+ this._configurationError = error;
183
+ this.notify();
184
+ throw error;
185
+ }
186
+ }
187
+ async connect(options) {
188
+ const config = this._config;
189
+ if (config == null) {
190
+ throw new Error("The controller has not been configured");
191
+ }
192
+ const cameraEnabled = options?.camera?.enabled === true;
193
+ const microphoneEnabled = options?.microphone?.enabled === true;
194
+ this.pendingLocalMedia.setCameraUnavailable(false);
195
+ this.pendingLocalMedia.setMicrophoneUnavailable(false);
196
+ this.pendingLocalMedia.setPending({
197
+ cameraPending: cameraEnabled,
198
+ microphonePending: microphoneEnabled,
199
+ cameraAwaitEnableConfirmation: cameraEnabled,
200
+ microphoneAwaitEnableConfirmation: microphoneEnabled
201
+ });
202
+ const { camera: _camera, microphone: _microphone, ...connectOptions } = options ?? {};
203
+ try {
204
+ await this.livekitRoom.connect(config.url, config.token, connectOptions);
205
+ const localParticipant = this.livekitRoom.localParticipant;
206
+ await Promise.all([
207
+ cameraEnabled ? localParticipant.setCameraEnabled(true).then(() => this.pendingLocalMedia.setCameraUnavailable(false)).catch((error) => {
208
+ this.pendingLocalMedia.setCameraPending(false);
209
+ this.pendingLocalMedia.setCameraUnavailable(true);
210
+ console.warn("unable to enable camera after connecting", error);
211
+ }) : Promise.resolve(this.pendingLocalMedia.setCameraPending(false)),
212
+ microphoneEnabled ? localParticipant.setMicrophoneEnabled(true).then(() => this.pendingLocalMedia.setMicrophoneUnavailable(false)).catch((error) => {
213
+ this.pendingLocalMedia.setMicrophonePending(false);
214
+ this.pendingLocalMedia.setMicrophoneUnavailable(true);
215
+ console.warn("unable to enable microphone after connecting", error);
216
+ }) : Promise.resolve(this.pendingLocalMedia.setMicrophonePending(false))
217
+ ]);
218
+ this.syncPendingLocalMediaState();
219
+ } catch (error) {
220
+ this.pendingLocalMedia.clear();
221
+ throw error;
222
+ }
223
+ }
224
+ async disconnect() {
225
+ this.pendingLocalMedia.clear();
226
+ await this.livekitRoom.disconnect();
227
+ }
228
+ dispose() {
229
+ this.livekitRoom.removeAllListeners();
230
+ this.pendingLocalMedia.dispose();
231
+ this.listeners.clear();
232
+ }
233
+ onRoomChanged = () => {
234
+ this.syncPendingLocalMediaState();
235
+ this.notify();
236
+ };
237
+ syncPendingLocalMediaState() {
238
+ this.pendingLocalMedia.syncFromLocalParticipant(
239
+ this.livekitRoom.localParticipant,
240
+ this.livekitRoom.state === ConnectionState.Disconnected
241
+ );
242
+ }
243
+ notify() {
244
+ for (const listener of this.listeners) {
245
+ listener();
246
+ }
247
+ }
248
+ }
249
+ const MeetingControllerContext = createContext(null);
250
+ function useMeetingController(controller) {
251
+ const contextController = useContext(MeetingControllerContext);
252
+ const resolved = controller ?? contextController;
253
+ if (resolved == null) {
254
+ throw new Error("useMeetingController must be used within MeetingScope or receive a controller");
255
+ }
256
+ useSyncExternalStore(
257
+ (listener) => resolved.subscribe(listener),
258
+ () => resolved.livekitRoom.state,
259
+ () => resolved.livekitRoom.state
260
+ );
261
+ return resolved;
262
+ }
263
+ function MeetingScope({
264
+ client,
265
+ breakoutRoom,
266
+ roomOptions,
267
+ children
268
+ }) {
269
+ const controller = useMemo(() => new MeetingController({ room: client, roomOptions }), [client, roomOptions]);
270
+ useEffect(() => {
271
+ void controller.configure({ breakoutRoom });
272
+ return () => {
273
+ if (controller.isConnected) {
274
+ void controller.disconnect().catch((error) => {
275
+ console.warn("unable to disconnect", error);
276
+ });
277
+ }
278
+ controller.dispose();
279
+ };
280
+ }, [breakoutRoom, controller]);
281
+ return /* @__PURE__ */ jsx(WakeLocker, { children: /* @__PURE__ */ jsx(MeetingControllerContext.Provider, { value: controller, children: typeof children === "function" ? children(controller) : children }) });
282
+ }
283
+ function firstEnabledVideoPublication(participant) {
284
+ return Array.from(participant.videoTrackPublications.values()).find(
285
+ (publication) => !publication.isMuted && publication.source === Track.Source.Camera && publication.videoTrack != null
286
+ );
287
+ }
288
+ export {
289
+ MeetingController,
290
+ MeetingScope,
291
+ PendingLocalMediaState,
292
+ firstEnabledVideoPublication,
293
+ useMeetingController
294
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./audio-visualization";
2
+ export * from "./controls";
3
+ export * from "./meeting-scope";
4
+ export * from "./participants";
5
+ export * from "./wake-lock";
@@ -0,0 +1,5 @@
1
+ export * from "./audio-visualization";
2
+ export * from "./controls";
3
+ export * from "./meeting-scope";
4
+ export * from "./participants";
5
+ export * from "./wake-lock";
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from "react";
2
+ import { Participant, Room } from "livekit-client";
3
+ import { MeetingController } from "./meeting-scope";
4
+ export declare function ParticipantCamerasList({ controller: providedController, spacing, className, }: {
5
+ controller?: MeetingController;
6
+ spacing?: number;
7
+ className?: string;
8
+ }): ReactElement;
9
+ export declare function ParticipantTile({ room, participant, className, }: {
10
+ room: Room;
11
+ participant: Participant;
12
+ className?: string;
13
+ }): ReactElement;