@whereby.com/browser-sdk 1.7.1 → 2.0.0-alpha

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 (64) hide show
  1. package/.eslintrc +4 -2
  2. package/.storybook/main.cjs +13 -2
  3. package/README.md +60 -7
  4. package/jest.config.js +2 -0
  5. package/package.json +38 -12
  6. package/rollup.config.js +37 -13
  7. package/src/lib/RoomConnection.ts +516 -0
  8. package/src/lib/RoomParticipant.ts +77 -0
  9. package/src/lib/__tests__/{index.unit.js → embed.unit.ts} +4 -8
  10. package/src/lib/api/ApiClient.ts +111 -0
  11. package/src/lib/api/Credentials.ts +45 -0
  12. package/src/lib/api/HttpClient.ts +95 -0
  13. package/src/lib/api/MultipartHttpClient.ts +53 -0
  14. package/src/lib/api/OrganizationApiClient.ts +64 -0
  15. package/src/lib/api/Response.ts +34 -0
  16. package/src/lib/api/credentialsService/index.ts +159 -0
  17. package/src/lib/api/credentialsService/test/index.spec.ts +181 -0
  18. package/src/lib/api/deviceService/index.ts +42 -0
  19. package/src/lib/api/deviceService/tests/index.spec.ts +74 -0
  20. package/src/lib/api/extractUtils.ts +160 -0
  21. package/src/lib/api/index.ts +8 -0
  22. package/src/lib/api/localStorageWrapper/index.ts +15 -0
  23. package/src/lib/api/models/Account.ts +48 -0
  24. package/src/lib/api/models/Meeting.ts +42 -0
  25. package/src/lib/api/models/Organization.ts +186 -0
  26. package/src/lib/api/models/Room.ts +44 -0
  27. package/src/lib/api/models/account/EmbeddedFreeTierStatus.ts +34 -0
  28. package/src/lib/api/models/tests/Account.spec.ts +128 -0
  29. package/src/lib/api/models/tests/Organization.spec.ts +161 -0
  30. package/src/lib/api/models/tests/Room.spec.ts +74 -0
  31. package/src/lib/api/modules/AbstractStore.ts +18 -0
  32. package/src/lib/api/modules/ChromeStorageStore.ts +44 -0
  33. package/src/lib/api/modules/LocalStorageStore.ts +57 -0
  34. package/src/lib/api/modules/tests/ChromeStorageStore.spec.ts +67 -0
  35. package/src/lib/api/modules/tests/LocalStorageStore.spec.ts +79 -0
  36. package/src/lib/api/modules/tests/__mocks__/storage.ts +24 -0
  37. package/src/lib/api/organizationService/index.ts +284 -0
  38. package/src/lib/api/organizationService/tests/index.spec.ts +781 -0
  39. package/src/lib/api/organizationServiceCache/index.ts +28 -0
  40. package/src/lib/api/organizationServiceCache/tests/index.spec.ts +101 -0
  41. package/src/lib/api/parameterAssertUtils.ts +166 -0
  42. package/src/lib/api/roomService/index.ts +310 -0
  43. package/src/lib/api/roomService/tests/index.spec.ts +668 -0
  44. package/src/lib/api/test/ApiClient.spec.ts +139 -0
  45. package/src/lib/api/test/HttpClient.spec.ts +120 -0
  46. package/src/lib/api/test/MultipartHttpClient.spec.ts +145 -0
  47. package/src/lib/api/test/OrganizationApiClient.spec.ts +132 -0
  48. package/src/lib/api/test/extractUtils.spec.ts +357 -0
  49. package/src/lib/api/test/helpers.ts +41 -0
  50. package/src/lib/api/test/parameterAssertUtils.spec.ts +265 -0
  51. package/src/lib/api/types.ts +6 -0
  52. package/src/lib/{index.js → embed.ts} +53 -26
  53. package/src/lib/index.ts +3 -0
  54. package/src/lib/react/VideoElement.tsx +16 -0
  55. package/src/lib/react/index.ts +3 -0
  56. package/src/lib/react/useLocalMedia.ts +25 -0
  57. package/src/lib/react/useRoomConnection.ts +92 -0
  58. package/src/lib/reducer.ts +142 -0
  59. package/src/stories/custom-ui.stories.tsx +133 -0
  60. package/src/stories/prebuilt-ui.stories.tsx +131 -0
  61. package/src/stories/styles.css +74 -0
  62. package/src/types.d.ts +175 -0
  63. package/tsconfig.json +30 -0
  64. package/src/index.stories.js +0 -101
@@ -1,8 +1,38 @@
1
1
  import { define, ref } from "heresy";
2
2
 
3
+ interface WherebyEmbedAttributes {
4
+ audio: string;
5
+ avatarUrl: string;
6
+ background: string;
7
+ cameraAccess: string;
8
+ chat: string;
9
+ displayName: string;
10
+ emptyRoomInvitation: string;
11
+ floatSelf: string;
12
+ help: string;
13
+ leaveButton: string;
14
+ logo: string;
15
+ people: string;
16
+ precallReview: string;
17
+ recording: string;
18
+ screenshare: string;
19
+ video: string;
20
+ virtualBackgroundUrl: string;
21
+ room: string;
22
+ style: { [key: string]: string };
23
+ }
24
+ declare global {
25
+ namespace JSX {
26
+ interface IntrinsicElements {
27
+ ["whereby-embed"]: Partial<WherebyEmbedAttributes>;
28
+ }
29
+ }
30
+ }
31
+
3
32
  const boolAttrs = [
4
33
  "audio",
5
34
  "background",
35
+ "cameraaccess",
6
36
  "chat",
7
37
  "people",
8
38
  "embed",
@@ -31,10 +61,10 @@ define("WherebyEmbed", {
31
61
  this.iframe = ref();
32
62
  },
33
63
  onconnected() {
34
- window.addEventListener("message", this);
64
+ window.addEventListener("message", this.onmessage);
35
65
  },
36
66
  ondisconnected() {
37
- window.removeEventListener("message", this);
67
+ window.removeEventListener("message", this.onmessage);
38
68
  },
39
69
  observedAttributes: [
40
70
  "displayName",
@@ -48,11 +78,11 @@ define("WherebyEmbed", {
48
78
  "avatarUrl",
49
79
  ...boolAttrs,
50
80
  ].map((a) => a.toLowerCase()),
51
- onattributechanged({ attributeName, oldValue }) {
81
+ onattributechanged({ attributeName, oldValue }: { attributeName: string; oldValue: string | boolean }) {
52
82
  if (["room", "subdomain"].includes(attributeName) && oldValue == null) return;
53
83
  this.render();
54
84
  },
55
- style(self) {
85
+ style(self: string) {
56
86
  return `
57
87
  ${self} {
58
88
  display: block;
@@ -66,7 +96,7 @@ define("WherebyEmbed", {
66
96
  },
67
97
 
68
98
  // Commands
69
- _postCommand(command, args = []) {
99
+ _postCommand(command: string, args = []) {
70
100
  if (this.iframe.current) {
71
101
  const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
72
102
  this.iframe.current.contentWindow.postMessage({ command, args }, url.origin);
@@ -78,17 +108,16 @@ define("WherebyEmbed", {
78
108
  stopRecording() {
79
109
  this._postCommand("stop_recording");
80
110
  },
81
- toggleCamera(enabled) {
111
+ toggleCamera(enabled: boolean) {
82
112
  this._postCommand("toggle_camera", [enabled]);
83
113
  },
84
- toggleMicrophone(enabled) {
114
+ toggleMicrophone(enabled: boolean) {
85
115
  this._postCommand("toggle_microphone", [enabled]);
86
116
  },
87
- toggleScreenshare(enabled) {
117
+ toggleScreenshare(enabled: boolean) {
88
118
  this._postCommand("toggle_screenshare", [enabled]);
89
119
  },
90
-
91
- onmessage({ origin, data }) {
120
+ onmessage({ origin, data }: { origin: string; data: { type: string; payload: string } }) {
92
121
  const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
93
122
  if (origin !== url.origin) return;
94
123
  const { type, payload: detail } = data;
@@ -105,9 +134,9 @@ define("WherebyEmbed", {
105
134
  groups,
106
135
  virtualbackgroundurl: virtualBackgroundUrl,
107
136
  } = this;
108
- if (!room) return this.html`Whereby: Missing room attr.`;
137
+ if (!room) return this.html`Whereby: Missing room attribute.`;
109
138
  // Get subdomain from room URL, or use it specified
110
- let m = /https:\/\/([^.]+)\.whereby.com\/.+/.exec(room);
139
+ const m = /https:\/\/([^.]+)\.whereby.com\/.+/.exec(room);
111
140
  const subdomain = (m && m[1]) || this.subdomain;
112
141
  if (!subdomain) return this.html`Whereby: Missing subdomain attr.`;
113
142
  const url = new URL(room, `https://${subdomain}.whereby.com`);
@@ -116,11 +145,11 @@ define("WherebyEmbed", {
116
145
  we: "__SDK_VERSION__",
117
146
  iframeSource: subdomain,
118
147
  ...(displayName && { displayName }),
119
- ...(lang && { lang: lang }),
120
- ...(metadata && { metadata: metadata }),
121
- ...(groups && { groups: groups }),
122
- ...(virtualBackgroundUrl && { virtualBackgroundUrl: virtualBackgroundUrl }),
123
- ...(avatarUrl && { avatarUrl: avatarUrl }),
148
+ ...(lang && { lang }),
149
+ ...(metadata && { metadata }),
150
+ ...(groups && { groups }),
151
+ ...(virtualBackgroundUrl && { virtualBackgroundUrl }),
152
+ ...(avatarUrl && { avatarUrl }),
124
153
  // the original ?embed name was confusing, so we give minimal
125
154
  ...(minimal != null && { embed: minimal }),
126
155
  ...boolAttrs.reduce(
@@ -129,17 +158,15 @@ define("WherebyEmbed", {
129
158
  {}
130
159
  ),
131
160
  }).forEach(([k, v]) => {
132
- if (!url.searchParams.has(k)) {
161
+ if (!url.searchParams.has(k) && typeof v === "string") {
133
162
  url.searchParams.set(k, v);
134
163
  }
135
164
  });
136
- this.html`
137
- <iframe
138
- ref=${this.iframe}
139
- src=${url}
140
- allow="autoplay; camera; microphone; fullscreen; speaker; display-capture" />
141
- `;
165
+ return this.html`
166
+ <iframe
167
+ ref=${this.iframe}
168
+ src=${url}
169
+ allow="autoplay; camera; microphone; fullscreen; speaker; display-capture" />
170
+ `;
142
171
  },
143
172
  });
144
-
145
- export default { sdkVersion: "__SDK_VERSION__" };
@@ -0,0 +1,3 @@
1
+ import "./embed";
2
+ export { useRoomConnection } from "./react";
3
+ export const sdkVersion = "__SDK_VERSION__";
@@ -0,0 +1,16 @@
1
+ import React, { useCallback } from "react";
2
+
3
+ interface VideoElProps {
4
+ stream: MediaStream;
5
+ style?: React.CSSProperties;
6
+ }
7
+
8
+ export default ({ stream, style }: VideoElProps) => {
9
+ const videoEl = useCallback<(node: HTMLVideoElement) => void>((node) => {
10
+ if (node !== null && node.srcObject !== stream) {
11
+ node.srcObject = stream;
12
+ }
13
+ }, []);
14
+
15
+ return <video ref={videoEl} autoPlay playsInline style={style} />;
16
+ };
@@ -0,0 +1,3 @@
1
+ export { default as VideoElement } from "./VideoElement";
2
+ export { default as useLocalMedia } from "./useLocalMedia";
3
+ export { default as useRoomConnection } from "./useRoomConnection";
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export default function useLocalMedia() {
4
+ const [localStream, setLocalStream] = useState<MediaStream | undefined>(undefined);
5
+
6
+ useEffect(() => {
7
+ const getLocalStream = async () => {
8
+ if (!localStream) {
9
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
10
+ setLocalStream(stream);
11
+ }
12
+ };
13
+
14
+ getLocalStream();
15
+
16
+ // Stop tracks on unmount
17
+ return () => {
18
+ localStream?.getTracks().forEach((t) => {
19
+ t.stop();
20
+ });
21
+ };
22
+ }, [localStream]);
23
+
24
+ return [localStream];
25
+ }
@@ -0,0 +1,92 @@
1
+ import { useEffect, useReducer, useState } from "react";
2
+ import RoomConnection, { RoomConnectionOptions } from "../RoomConnection";
3
+ import reducer, { RoomState } from "../reducer";
4
+ import VideoElement from "./VideoElement";
5
+
6
+ interface RoomConnectionActions {
7
+ toggleCamera(enabled?: boolean): void;
8
+ toggleMicrophone(enabled?: boolean): void;
9
+ setDisplayName(displayName: string): void;
10
+ }
11
+
12
+ interface RoomConnectionComponents {
13
+ VideoView: typeof VideoElement;
14
+ }
15
+
16
+ export default function useRoomConnection(
17
+ roomUrl: string,
18
+ roomConnectionOptions: RoomConnectionOptions
19
+ ): [state: RoomState, actions: RoomConnectionActions, components: RoomConnectionComponents] {
20
+ const [roomConnection, setRoomConnection] = useState<RoomConnection | null>(null);
21
+ const [state, dispatch] = useReducer(reducer, { remoteParticipants: [] });
22
+
23
+ useEffect(() => {
24
+ setRoomConnection(new RoomConnection(roomUrl, roomConnectionOptions));
25
+ }, [roomUrl]);
26
+
27
+ useEffect(() => {
28
+ if (!roomConnection) {
29
+ return;
30
+ }
31
+
32
+ roomConnection.addEventListener("participant_audio_enabled", (e) => {
33
+ const { participantId, isAudioEnabled } = e.detail;
34
+ dispatch({ type: "PARTICIPANT_AUDIO_ENABLED", payload: { participantId, isAudioEnabled } });
35
+ });
36
+
37
+ roomConnection.addEventListener("participant_joined", (e) => {
38
+ const { remoteParticipant } = e.detail;
39
+ dispatch({ type: "PARTICIPANT_JOINED", payload: { paritipant: remoteParticipant } });
40
+ });
41
+
42
+ roomConnection.addEventListener("participant_left", (e) => {
43
+ const { participantId } = e.detail;
44
+ dispatch({ type: "PARTICIPANT_LEFT", payload: { participantId } });
45
+ });
46
+
47
+ roomConnection.addEventListener("participant_stream_added", (e) => {
48
+ const { participantId, stream } = e.detail;
49
+ dispatch({ type: "PARTICIPANT_STREAM_ADDED", payload: { participantId, stream } });
50
+ });
51
+
52
+ roomConnection.addEventListener("room_joined", (e) => {
53
+ const { localParticipant, remoteParticipants } = e.detail;
54
+ dispatch({ type: "ROOM_JOINED", payload: { localParticipant, remoteParticipants } });
55
+ });
56
+
57
+ roomConnection.addEventListener("participant_video_enabled", (e) => {
58
+ const { participantId, isVideoEnabled } = e.detail;
59
+ dispatch({ type: "PARTICIPANT_VIDEO_ENABLED", payload: { participantId, isVideoEnabled } });
60
+ });
61
+
62
+ roomConnection.addEventListener("participant_metadata_changed", (e) => {
63
+ const { participantId, displayName } = e.detail;
64
+ dispatch({ type: "PARTICIPANT_METADATA_CHANGED", payload: { participantId, displayName } });
65
+ });
66
+
67
+ roomConnection.join();
68
+
69
+ return () => {
70
+ roomConnection.leave();
71
+ };
72
+ }, [roomConnection]);
73
+
74
+ return [
75
+ state,
76
+ {
77
+ toggleCamera: (enabled) => {
78
+ roomConnection?.toggleCamera(enabled);
79
+ },
80
+ toggleMicrophone: (enabled) => {
81
+ roomConnection?.toggleMicrophone(enabled);
82
+ },
83
+ setDisplayName: (displayName) => {
84
+ roomConnection?.setDisplayName(displayName);
85
+ dispatch({ type: "LOCAL_CLIENT_DISPLAY_NAME_CHANGED", payload: { displayName } });
86
+ },
87
+ },
88
+ {
89
+ VideoView: VideoElement,
90
+ },
91
+ ];
92
+ }
@@ -0,0 +1,142 @@
1
+ import { LocalParticipant, RemoteParticipant } from "./RoomParticipant";
2
+
3
+ type RemoteParticipantState = Omit<RemoteParticipant, "updateStreamState">;
4
+
5
+ export interface RoomState {
6
+ localParticipant?: LocalParticipant;
7
+ roomConnectionStatus?: "connecting" | "connected" | "disconnected";
8
+ remoteParticipants: RemoteParticipantState[];
9
+ }
10
+
11
+ type Action =
12
+ | {
13
+ type: "ROOM_JOINED";
14
+ payload: {
15
+ localParticipant: LocalParticipant;
16
+ remoteParticipants: RemoteParticipantState[];
17
+ };
18
+ }
19
+ | {
20
+ type: "PARTICIPANT_AUDIO_ENABLED";
21
+ payload: {
22
+ participantId: string;
23
+ isAudioEnabled: boolean;
24
+ };
25
+ }
26
+ | {
27
+ type: "PARTICIPANT_JOINED";
28
+ payload: {
29
+ paritipant: RemoteParticipantState;
30
+ };
31
+ }
32
+ | {
33
+ type: "PARTICIPANT_LEFT";
34
+ payload: {
35
+ participantId: string;
36
+ };
37
+ }
38
+ | {
39
+ type: "PARTICIPANT_STREAM_ADDED";
40
+ payload: {
41
+ participantId: string;
42
+ stream: MediaStream;
43
+ };
44
+ }
45
+ | {
46
+ type: "PARTICIPANT_VIDEO_ENABLED";
47
+ payload: {
48
+ participantId: string;
49
+ isVideoEnabled: boolean;
50
+ };
51
+ }
52
+ | {
53
+ type: "PARTICIPANT_METADATA_CHANGED";
54
+ payload: {
55
+ participantId: string;
56
+ displayName: string;
57
+ };
58
+ }
59
+ | {
60
+ type: "LOCAL_CLIENT_DISPLAY_NAME_CHANGED";
61
+ payload: {
62
+ displayName: string;
63
+ };
64
+ };
65
+
66
+ function updateParticipant(
67
+ remoteParticipants: RemoteParticipantState[],
68
+ participantId: string,
69
+ updates: Partial<RemoteParticipantState>
70
+ ): RemoteParticipantState[] {
71
+ const existingParticipant = remoteParticipants.find((p) => p.id === participantId);
72
+ if (!existingParticipant) {
73
+ return remoteParticipants;
74
+ }
75
+ const index = remoteParticipants.indexOf(existingParticipant);
76
+
77
+ return [
78
+ ...remoteParticipants.slice(0, index),
79
+ { ...existingParticipant, ...updates },
80
+ ...remoteParticipants.slice(index + 1),
81
+ ];
82
+ }
83
+
84
+ export default function reducer(state: RoomState, action: Action): RoomState {
85
+ switch (action.type) {
86
+ case "ROOM_JOINED":
87
+ return {
88
+ ...state,
89
+ localParticipant: action.payload.localParticipant,
90
+ remoteParticipants: action.payload.remoteParticipants,
91
+ roomConnectionStatus: "connected",
92
+ };
93
+ case "PARTICIPANT_AUDIO_ENABLED":
94
+ return {
95
+ ...state,
96
+ remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
97
+ isAudioEnabled: action.payload.isAudioEnabled,
98
+ }),
99
+ };
100
+ case "PARTICIPANT_JOINED":
101
+ return {
102
+ ...state,
103
+ remoteParticipants: [...state.remoteParticipants, action.payload.paritipant],
104
+ };
105
+ case "PARTICIPANT_LEFT":
106
+ return {
107
+ ...state,
108
+ remoteParticipants: [...state.remoteParticipants.filter((p) => p.id !== action.payload.participantId)],
109
+ };
110
+ case "PARTICIPANT_STREAM_ADDED":
111
+ return {
112
+ ...state,
113
+ remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
114
+ stream: action.payload.stream,
115
+ }),
116
+ };
117
+ case "PARTICIPANT_VIDEO_ENABLED":
118
+ return {
119
+ ...state,
120
+ remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
121
+ isVideoEnabled: action.payload.isVideoEnabled,
122
+ }),
123
+ };
124
+ case "PARTICIPANT_METADATA_CHANGED":
125
+ return {
126
+ ...state,
127
+ remoteParticipants: [
128
+ ...state.remoteParticipants.map((p) =>
129
+ p.id === action.payload.participantId ? { ...p, displayName: action.payload.displayName } : p
130
+ ),
131
+ ],
132
+ };
133
+ case "LOCAL_CLIENT_DISPLAY_NAME_CHANGED":
134
+ if (!state.localParticipant) return state;
135
+ return {
136
+ ...state,
137
+ localParticipant: { ...state.localParticipant, displayName: action.payload.displayName },
138
+ };
139
+ default:
140
+ throw state;
141
+ }
142
+ }
@@ -0,0 +1,133 @@
1
+ import React, { useState } from "react";
2
+ import { useLocalMedia, useRoomConnection, VideoElement } from "../lib/react";
3
+ import "./styles.css";
4
+
5
+ export default {
6
+ title: "Examples/Custom UI",
7
+ argTypes: {
8
+ displayName: { control: "text", defaultValue: "SDK" },
9
+ roomUrl: { control: "text", defaultValue: process.env.STORYBOOK_ROOM, type: { required: true } },
10
+ },
11
+ };
12
+
13
+ const DisplayNameForm = ({
14
+ initialDisplayName,
15
+ onSetDisplayName,
16
+ }: {
17
+ initialDisplayName?: string;
18
+ onSetDisplayName: (displayName: string) => void;
19
+ }) => {
20
+ const [displayName, setDisplayName] = useState(initialDisplayName || "");
21
+
22
+ return (
23
+ <div>
24
+ <label htmlFor="displayName">Display name: </label>
25
+ <input
26
+ type="text"
27
+ name="displayName"
28
+ id="displayName"
29
+ value={displayName}
30
+ onChange={(e) => setDisplayName(e.target.value)}
31
+ />
32
+ <button onClick={() => onSetDisplayName(displayName || "")}>Save</button>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ const VideoExperience = ({
38
+ displayName,
39
+ roomName,
40
+ localStream,
41
+ }: {
42
+ displayName?: string;
43
+ roomName: string;
44
+ localStream?: MediaStream;
45
+ }) => {
46
+ const [state, actions, components] = useRoomConnection(roomName, {
47
+ displayName,
48
+ localMediaConstraints: {
49
+ audio: true,
50
+ video: true,
51
+ },
52
+ localStream,
53
+ logger: console,
54
+ });
55
+
56
+ const { localParticipant, remoteParticipants } = state;
57
+ const { setDisplayName, toggleCamera, toggleMicrophone } = actions;
58
+ const { VideoView } = components;
59
+
60
+ return (
61
+ <div>
62
+ <div className="container">
63
+ {[localParticipant, ...remoteParticipants].map((participant, i) => (
64
+ <div className="participantWrapper" key={participant?.id || i}>
65
+ {participant ? (
66
+ <>
67
+ <div
68
+ className="bouncingball"
69
+ style={{
70
+ animationDelay: `${Math.random() * 1000}ms`,
71
+ ...(participant.isAudioEnabled
72
+ ? {
73
+ border: "2px solid grey",
74
+ }
75
+ : null),
76
+ ...(!participant.isVideoEnabled
77
+ ? {
78
+ backgroundColor: "green",
79
+ }
80
+ : null),
81
+ }}
82
+ >
83
+ {participant.stream && participant.isVideoEnabled && (
84
+ <VideoView
85
+ stream={participant.stream}
86
+ style={{ width: "100%", height: "100%", objectFit: "cover" }}
87
+ />
88
+ )}
89
+ </div>
90
+ <div className="displayName">{participant.displayName || "Guest"}</div>
91
+ </>
92
+ ) : null}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ <div className="controls">
97
+ <button onClick={() => toggleCamera()}>Toggle camera</button>
98
+ <button onClick={() => toggleMicrophone()}>Toggle microphone</button>
99
+ <DisplayNameForm initialDisplayName={displayName} onSetDisplayName={setDisplayName} />
100
+ </div>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ const roomRegEx = new RegExp(/^https:\/\/.*\/.*/);
106
+
107
+ export const Simplistic = ({ roomUrl, displayName }: { roomUrl: string; displayName?: string }) => {
108
+ if (!roomUrl || !roomUrl.match(roomRegEx)) {
109
+ return <p>Set room url on the Controls panel</p>;
110
+ }
111
+
112
+ return <VideoExperience displayName={displayName} roomName={roomUrl} />;
113
+ };
114
+
115
+ export const WithPreCall = ({ roomUrl, displayName }: { roomUrl: string; displayName?: string }) => {
116
+ const [localStream] = useLocalMedia();
117
+ const [shouldJoin, setShouldJoin] = useState(false);
118
+
119
+ if (!roomUrl || !roomUrl.match(roomRegEx)) {
120
+ return <p>Set room url on the Controls panel</p>;
121
+ }
122
+
123
+ return (
124
+ <div>
125
+ {shouldJoin ? (
126
+ <VideoExperience displayName={displayName} roomName={roomUrl} localStream={localStream} />
127
+ ) : (
128
+ <div>{localStream && <VideoElement stream={localStream} />}</div>
129
+ )}
130
+ <button onClick={() => setShouldJoin(!shouldJoin)}>{shouldJoin ? "Leave room" : "Join room"}</button>
131
+ </div>
132
+ );
133
+ };