@digitalsamba/embedded-sdk 0.0.12 → 0.0.17

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 (39) hide show
  1. package/README.md +9 -0
  2. package/dist/cjs/index.d.ts +26 -2
  3. package/dist/cjs/index.js +185 -9
  4. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  5. package/dist/cjs/types.d.ts +104 -9
  6. package/dist/cjs/types.js +0 -6
  7. package/dist/cjs/utils/PermissionManager/index.d.ts +13 -0
  8. package/dist/cjs/utils/PermissionManager/index.js +74 -0
  9. package/dist/cjs/utils/PermissionManager/types.d.ts +23 -0
  10. package/dist/cjs/utils/PermissionManager/types.js +2 -0
  11. package/dist/cjs/utils/proxy.d.ts +1 -0
  12. package/dist/cjs/utils/proxy.js +21 -0
  13. package/dist/cjs/utils/vars.d.ts +23 -0
  14. package/dist/cjs/utils/vars.js +48 -0
  15. package/dist/esm/index.d.ts +26 -2
  16. package/dist/esm/index.js +184 -8
  17. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  18. package/dist/esm/types.d.ts +104 -9
  19. package/dist/esm/types.js +1 -5
  20. package/dist/esm/utils/PermissionManager/index.d.ts +13 -0
  21. package/dist/esm/utils/PermissionManager/index.js +70 -0
  22. package/dist/esm/utils/PermissionManager/types.d.ts +23 -0
  23. package/dist/esm/utils/PermissionManager/types.js +1 -0
  24. package/dist/esm/utils/proxy.d.ts +1 -0
  25. package/dist/esm/utils/proxy.js +17 -0
  26. package/dist/esm/utils/vars.d.ts +23 -0
  27. package/dist/esm/utils/vars.js +45 -0
  28. package/dist/index.html +264 -59
  29. package/dist/initial-config-demo.html +303 -0
  30. package/dist/permissions-demo.html +289 -0
  31. package/dist/types/index.d.ts +26 -2
  32. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  33. package/dist/types/types.d.ts +104 -9
  34. package/dist/types/utils/PermissionManager/index.d.ts +13 -0
  35. package/dist/types/utils/PermissionManager/types.d.ts +23 -0
  36. package/dist/types/utils/proxy.d.ts +1 -0
  37. package/dist/types/utils/vars.d.ts +23 -0
  38. package/dist/umd/index.js +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,70 @@
1
+ export class PermissionManager {
2
+ constructor(parent) {
3
+ this.permissionsMap = {};
4
+ this.lookupDynamicPermission = (permission, dynamicPermissions) => {
5
+ if (!dynamicPermissions) {
6
+ return false;
7
+ }
8
+ return dynamicPermissions.includes(permission);
9
+ };
10
+ this.lookupPermission = (options) => {
11
+ const { permissionsMap, permission, targetRole, role, dynamicPermissions } = options;
12
+ if (dynamicPermissions) {
13
+ const granted = this.lookupDynamicPermission(permission, dynamicPermissions);
14
+ if (granted) {
15
+ return true;
16
+ }
17
+ }
18
+ if (permissionsMap[role][permission]) {
19
+ return true;
20
+ }
21
+ return Boolean(permissionsMap[role][`${permission}_${targetRole}`]);
22
+ };
23
+ this.checkPermissions = ({ permissions, targetRole, permissionsMap, role, dynamicPermissions, }) => {
24
+ if (Array.isArray(permissions)) {
25
+ return permissions.some((permission) => this.lookupPermission({ permission, targetRole, role, permissionsMap, dynamicPermissions }));
26
+ }
27
+ return this.lookupPermission({
28
+ permission: permissions,
29
+ targetRole,
30
+ role,
31
+ permissionsMap,
32
+ dynamicPermissions,
33
+ });
34
+ };
35
+ this.refinePermissions = (permissions, { targetRole, role, userId, users, permissionsMap, localUser }) => {
36
+ const options = {
37
+ permissionsMap,
38
+ permissions,
39
+ targetRole,
40
+ role: localUser.role,
41
+ dynamicPermissions: localUser.dynamicPermissions,
42
+ };
43
+ if (role) {
44
+ options.role = role;
45
+ options.dynamicPermissions = undefined;
46
+ }
47
+ if (userId && users) {
48
+ options.role = users[userId].role;
49
+ options.dynamicPermissions = users[userId].dynamicPermissions;
50
+ }
51
+ return this.checkPermissions(options);
52
+ };
53
+ this.hasPermissions = (permissions, { targetRole, role, userId } = {}) => {
54
+ const users = this.parent.stored.users;
55
+ const localUser = this.parent.localUser;
56
+ if (!localUser) {
57
+ return false;
58
+ }
59
+ return this.refinePermissions(permissions, {
60
+ permissionsMap: this.permissionsMap,
61
+ role,
62
+ targetRole,
63
+ users,
64
+ userId,
65
+ localUser,
66
+ });
67
+ };
68
+ this.parent = parent;
69
+ }
70
+ }
@@ -0,0 +1,23 @@
1
+ import { User, UserId } from '../../types';
2
+ import { PermissionTypes } from '../vars';
3
+ export type PermissionsMap = Record<string, Record<string, boolean>>;
4
+ export interface HasPermissionsOptions {
5
+ targetRole?: string;
6
+ role?: string;
7
+ userId?: UserId;
8
+ }
9
+ export interface RefinePermissionsOptions extends HasPermissionsOptions {
10
+ permissionsMap: PermissionsMap;
11
+ localUser: User;
12
+ users?: Record<UserId, User>;
13
+ }
14
+ export interface LookupPermissionOptions {
15
+ permissionsMap: PermissionsMap;
16
+ permission: PermissionTypes;
17
+ role: string;
18
+ targetRole?: string;
19
+ dynamicPermissions?: PermissionTypes[];
20
+ }
21
+ export interface CheckPermissionsOptions extends Omit<LookupPermissionOptions, 'permission'> {
22
+ permissions: PermissionTypes | PermissionTypes[];
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const createWatchedProxy: <G>(initialState: G, onChange: (newState: G) => void) => any;
@@ -0,0 +1,17 @@
1
+ const createHandler = (onChange) => {
2
+ const handler = {
3
+ get(target, key) {
4
+ if (typeof target[key] === 'object' && target[key] !== null) {
5
+ return new Proxy(target[key], handler);
6
+ }
7
+ return target[key];
8
+ },
9
+ set(target, prop, value) {
10
+ target[prop] = value;
11
+ onChange(target);
12
+ return target;
13
+ },
14
+ };
15
+ return handler;
16
+ };
17
+ export const createWatchedProxy = (initialState, onChange) => new Proxy(initialState, createHandler(onChange));
@@ -0,0 +1,23 @@
1
+ import { Stored } from '../types';
2
+ export declare const CONNECT_TIMEOUT = 10000;
3
+ export declare const internalEvents: Record<string, boolean>;
4
+ export declare enum LayoutMode {
5
+ tiled = "tiled",
6
+ auto = "auto"
7
+ }
8
+ export declare enum PermissionTypes {
9
+ broadcast = "broadcast",
10
+ manageBroadcast = "manage_broadcast",
11
+ endSession = "end_session",
12
+ startSession = "start_session",
13
+ removeParticipant = "remove_participant",
14
+ screenshare = "screenshare",
15
+ manageScreenshare = "manage_screenshare",
16
+ recording = "recording",
17
+ generalChat = "general_chat",
18
+ remoteMuting = "remote_muting",
19
+ askRemoteUnmute = "ask_remote_unmute",
20
+ raiseHand = "raise_hand",
21
+ manageRoles = "manage_roles"
22
+ }
23
+ export declare const defaultStoredState: Stored;
@@ -0,0 +1,45 @@
1
+ export const CONNECT_TIMEOUT = 10000;
2
+ export const internalEvents = {
3
+ roomJoined: true,
4
+ };
5
+ export var LayoutMode;
6
+ (function (LayoutMode) {
7
+ LayoutMode["tiled"] = "tiled";
8
+ LayoutMode["auto"] = "auto";
9
+ })(LayoutMode || (LayoutMode = {}));
10
+ export var PermissionTypes;
11
+ (function (PermissionTypes) {
12
+ PermissionTypes["broadcast"] = "broadcast";
13
+ PermissionTypes["manageBroadcast"] = "manage_broadcast";
14
+ PermissionTypes["endSession"] = "end_session";
15
+ PermissionTypes["startSession"] = "start_session";
16
+ PermissionTypes["removeParticipant"] = "remove_participant";
17
+ PermissionTypes["screenshare"] = "screenshare";
18
+ PermissionTypes["manageScreenshare"] = "manage_screenshare";
19
+ PermissionTypes["recording"] = "recording";
20
+ PermissionTypes["generalChat"] = "general_chat";
21
+ PermissionTypes["remoteMuting"] = "remote_muting";
22
+ PermissionTypes["askRemoteUnmute"] = "ask_remote_unmute";
23
+ PermissionTypes["raiseHand"] = "raise_hand";
24
+ PermissionTypes["manageRoles"] = "manage_roles";
25
+ })(PermissionTypes || (PermissionTypes = {}));
26
+ export const defaultStoredState = {
27
+ userId: '',
28
+ roomState: {
29
+ media: {
30
+ micEnabled: false,
31
+ cameraEnabled: false,
32
+ },
33
+ layout: {
34
+ mode: LayoutMode.tiled,
35
+ showToolbar: true,
36
+ toolbarPosition: 'left',
37
+ },
38
+ captionsState: {
39
+ showCaptions: false,
40
+ spokenLanguage: 'en',
41
+ fontSize: 'medium',
42
+ },
43
+ },
44
+ users: {},
45
+ };
package/dist/index.html CHANGED
@@ -7,68 +7,106 @@
7
7
  <title>Document</title>
8
8
  <!-- <script src="https://unpkg.com/@digitalsamba/embedded-sdk"></script>-->
9
9
  <script src="./umd/index.js"></script>
10
+ <style>
11
+ .user-list-row {
12
+ display: flex;
13
+ margin: 12px 0 8px;
14
+ align-items: center;
15
+ }
16
+
17
+ .user-list-avatar {
18
+ width: 24px;
19
+ height: 24px;
20
+ display: block;
21
+ border-radius: 50%;
22
+ margin-right: 8px;
23
+ }
24
+
25
+ .speaker-indicator {
26
+ width: 8px;
27
+ height: 8px;
28
+ display: block;
29
+ border-radius: 50%;
30
+ margin-right: 8px;
31
+ background: #4cd964;
32
+ opacity: 0;
33
+ margin-left: 8px;
34
+ }
35
+
36
+ .speaker-indicator.show {
37
+ opacity: 1
38
+ }
39
+
40
+
41
+ .log p {
42
+ font-size: 14px;
43
+ line-height: 1;
44
+ margin: 0 0 4px
45
+ }
46
+
47
+ .buttons button, .captions-buttons button {
48
+ margin: 2px;
49
+ }
50
+ </style>
10
51
  </head>
11
52
  <body>
12
- <div class="ifp" style="display: flex">
13
-
14
- <div class="log">
53
+ <div style="display: flex">
54
+ <div class="ifp">
15
55
 
16
56
  </div>
57
+ <div style="min-width: 300px ; padding: 0 16px">
58
+ <h3>Room users:</h3>
59
+ <div class="participants">
17
60
 
61
+ </div>
62
+ <h3>Room state</h3>
63
+ <div class="state">
64
+
65
+ </div>
66
+ </div>
18
67
  </div>
19
- <div class="div" style=" border: 1px solid yellow">
68
+ <div class="div buttons" style=" border: 1px solid yellow">
20
69
  <button class="c0" style="margin-right: 15px; border: 2px solid blue">LOAD</button>
21
- <button class="c1">toggleVideo</button>
22
- <button class="c2">enableVideo</button>
23
- <button class="c3">disableVideo</button>
24
- <button class="c4">toggleAudio</button>
25
- <button class="c5">enableAudio</button>
26
- <button class="c6">disableAudio</button>
27
- <button class="c7">startScreenshare</button>
28
- <button class="c8">stopScreenshare</button>
29
- <button class="c9">startRecording</button>
30
- <button class="c10">stopRecording</button>
31
- <button class="c11">showToolbar</button>
32
- <button class="c12">hideToolbar</button>
33
- <button class="c13">toggleToolbar</button>
34
- <button class="c14">auto layout</button>
35
- <button class="c15">tiled layout</button>
36
- <button class="c16">leave session</button>
37
- <button class="c17">end session</button>
70
+
71
+ </div>
72
+ <div class="div captions-buttons" style=" border: 1px solid yellow">
73
+ <p>captions controls</p>
74
+
75
+ </div>
76
+
77
+ <div class="log">
78
+
38
79
  </div>
39
80
 
40
81
  <script async defer>
41
82
  const parent = document.querySelector('.ifp');
42
83
  const frame = document.querySelector('.if');
84
+ const buttonsParent = document.querySelector('.buttons');
85
+ const captionsButtonsParent = document.querySelector('.captions-buttons');
86
+ const participantList = document.querySelector('.participants');
87
+
43
88
  const btn0 = document.querySelector('.c0');
44
- const btn1 = document.querySelector('.c1');
45
- const btn2 = document.querySelector('.c2');
46
- const btn3 = document.querySelector('.c3');
47
- const btn4 = document.querySelector('.c4');
48
- const btn5 = document.querySelector('.c5');
49
- const btn6 = document.querySelector('.c6');
50
- const btn7 = document.querySelector('.c7');
51
- const btn8 = document.querySelector('.c8');
52
- const btn9 = document.querySelector('.c9');
53
- const btn10 = document.querySelector('.c10');
54
- const btn11 = document.querySelector('.c11');
55
- const btn12 = document.querySelector('.c12');
56
- const btn13 = document.querySelector('.c13');
57
- const btn14 = document.querySelector('.c14');
58
- const btn15 = document.querySelector('.c15');
59
- const btn16 = document.querySelector('.c16');
60
- const btn17 = document.querySelector('.c17');
89
+
61
90
 
62
91
  // change these values to connect to your room
63
92
  const TEAM = 'some-team';
64
93
  const ROOM = 'some-room';
65
- const ROOM_URL = 'https://localhost:3000/Public'
94
+ let ROOM_URL = 'https://localhost:3000/Public'
95
+
96
+ if(window.location.search) {
97
+ const params = new URLSearchParams(window.location.search);
98
+ const dynamicRoomUrl = params.get('roomUrl');
99
+ if(dynamicRoomUrl) {
100
+ ROOM_URL = dynamicRoomUrl
101
+ }
102
+ }
66
103
 
67
104
  //const api = new DigitalSambaEmbedded({ root: parent, team: TEAM, room: ROOM});
68
105
  // const api = new DigitalSambaEmbedded({ frame});
69
106
  //const api = new DigitalSambaEmbedded({ url: ROOM_URL}, {reportErrors: true});
70
107
 
71
- const api = DigitalSambaEmbedded.createControl({ url: ROOM_URL});
108
+
109
+ var api = DigitalSambaEmbedded.createControl({ url: ROOM_URL, root: parent });
72
110
 
73
111
  const log = document.querySelector('.log');
74
112
 
@@ -76,29 +114,60 @@
76
114
  api.frame.height = 700;
77
115
 
78
116
  btn0.onclick = () => {api.load({ frameAttributes: {style: "border: 5px solid red"}, reportErrors: true })}
79
- btn1.onclick = () => {api.toggleVideo()}
80
- btn2.onclick = () => {api.enableVideo()}
81
- btn3.onclick = () => {api.disableVideo()}
82
- btn4.onclick = () => {api.toggleAudio()}
83
- btn5.onclick = () => {api.enableAudio()}
84
- btn6.onclick = () => {api.disableAudio()}
85
- btn7.onclick = () => {api.startScreenshare()}
86
- btn8.onclick = () => {api.stopScreenshare()}
87
- btn9.onclick = () => {api.startRecording()}
88
- btn10.onclick = () => {api.stopRecording()}
89
- btn11.onclick = () => {api.showToolbar()}
90
- btn12.onclick = () => {api.hideToolbar()}
91
- btn13.onclick = () => {api.toggleToolbar()}
92
- btn14.onclick = () => {api.changeLayoutMode('auto')}
93
- btn15.onclick = () => {api.changeLayoutMode('tiled')}
94
- btn16.onclick = () => {api.leaveSession()}
95
- btn17.onclick = () => {api.endSession()}
117
+
118
+ const baseControls = [
119
+ {command: 'toggleVideo', label: 'toggle video' },
120
+ {command: 'enableVideo', label: 'enable video' },
121
+ {command: 'disableVideo', label: 'disable video' },
122
+ {command: 'toggleAudio', label: 'toggle audio' },
123
+ {command: 'enableAudio', label: 'enable audio' },
124
+ {command: 'disableAudio', label: 'disable audio' },
125
+ {command: 'startScreenshare', label: 'start screenshare' },
126
+ {command: 'stopScreenshare', label: 'stop screenshare' },
127
+ {command: 'raiseHand', label: 'raise hand' },
128
+ {command: 'startRecording', label: 'start recording' },
129
+ {command: 'stopRecording', label: 'stop recording' },
130
+ {command: 'showToolbar', label: 'show toolbar' },
131
+ {command: 'hideToolbar', label: 'hide toolbar' },
132
+ {command: 'toggleToolbar', label: 'toggle toolbar' },
133
+ {command: 'changeLayoutMode', label: 'auto layout', args: ['auto'] },
134
+ {command: 'changeLayoutMode', label: 'tiled layout', args: ['tiled'] },
135
+ {command: 'showCaptions', label: 'show captions'},
136
+ {command: 'hideCaptions', label: 'hide captions'},
137
+ {command: 'toggleCaptions', label: 'toggle captions'},
138
+ {command: 'leaveSession', label: 'leave session' },
139
+ {command: 'endSession', label: 'end session' },
140
+ ]
141
+
142
+ baseControls.forEach(control => {
143
+ const button = document.createElement("button");
144
+ button.innerHTML = control.label;
145
+ button.onclick = () => api[control.command](...(control.args || []))
146
+
147
+ buttonsParent.appendChild(button);
148
+ })
149
+
150
+ const captionsControls = [
151
+ {command: 'configureCaptions', label: 'set font size - small', args: [{fontSize: 'small'}] },
152
+ {command: 'configureCaptions', label: 'set font size - medium', args: [{fontSize: 'medium'}] },
153
+ {command: 'configureCaptions', label: 'set font size - large', args: [{fontSize: 'large'}] },
154
+ {command: 'configureCaptions', label: 'set spoken language - english', args: [{spokenLanguage: 'en'}] },
155
+ {command: 'configureCaptions', label: 'set spoken language - french', args: [{spokenLanguage: 'fr'}] },
156
+ {command: 'configureCaptions', label: 'set spoken language - spanish', args: [{spokenLanguage: 'es'}] },
157
+ ]
158
+
159
+ captionsControls.forEach(control => {
160
+ const button = document.createElement("button");
161
+ button.innerHTML = control.label;
162
+ button.onclick = () => api[control.command](...(control.args || []))
163
+
164
+ captionsButtonsParent.appendChild(button);
165
+ })
96
166
 
97
167
  api.on('*', (data) => {
98
- log.innerHTML += `<p>${Number(new Date)}: ev(${data.type}): ${JSON.stringify(data)}</p>`
168
+ log.innerHTML += `<p>${Number(new Date)}: ev(${data?.type}): ${JSON.stringify(data)}</p>`
99
169
  });
100
170
 
101
-
102
171
  api.on('userJoined', (data) => {
103
172
  log.innerHTML += `<p>${Number(new Date)}: USER JOINED: ${JSON.stringify(data)}</p>`
104
173
  });
@@ -107,6 +176,142 @@
107
176
  log.innerHTML += `<p>${Number(new Date)}: USER LEFT: ${JSON.stringify(data)}</p>`
108
177
  });
109
178
 
179
+ api.on('permissionsChanged', ({ data }) => {
180
+ if(data.broadcast !== undefined){
181
+ if(data.broadcast) {
182
+ alert('You were granted broadcast permission')
183
+ } else {
184
+ alert('Your permission to broadcast was rejected')
185
+ }
186
+ }
187
+
188
+ if(data.screenshare !== undefined){
189
+ if(data.screenshare) {
190
+ alert('You were granted screenshare permission')
191
+ } else {
192
+ alert('Your permission to screenshare was rejected')
193
+ }
194
+ }
195
+ });
196
+
197
+ api.on('handRaised', ({data}) => {
198
+ const control = document.querySelector('#lower-hand-'+data.userId);
199
+
200
+ if(control) {
201
+ control.style.display = 'inline'
202
+ }
203
+ })
204
+
205
+ api.on('handLowered', ({data}) => {
206
+ const id = data.user;
207
+
208
+ const control = document.querySelector('#lower-hand-'+ id );
209
+
210
+ if(control) {
211
+ control.style.display = 'none'
212
+ }
213
+ })
214
+
215
+ const stateBlock = document.querySelector('.state');
216
+
217
+ api.on('roomStateUpdated', ({data}) => {
218
+ stateBlock.innerHTML = JSON.stringify(data);
219
+ })
220
+
221
+ api.on('usersUpdated', ({data: {users}}) => {
222
+ participantList.innerHTML = '';
223
+
224
+ users.forEach(user => {
225
+ const row = document.createElement('div');
226
+ row.className = 'user-list-row';
227
+ const subButtons = document.createElement('div')
228
+ row.appendChild(subButtons);
229
+
230
+ const avatar = document.createElement('span');
231
+ avatar.className = 'user-list-avatar';
232
+ avatar.style.background = user.avatarColor;
233
+ row.appendChild(avatar);
234
+
235
+
236
+ const name = document.createElement('span');
237
+ name.className = 'user-list-name';
238
+ name.innerText = user.name;
239
+ row.appendChild(name);
240
+
241
+ const speakerIndicator = document.createElement('span')
242
+ speakerIndicator.className = `speaker-indicator speaker-indicator-${user.id}`;
243
+ row.appendChild(speakerIndicator);
244
+
245
+
246
+ if(user.kind === 'local') {
247
+ const label = document.createElement('span');
248
+ label.className = 'user-list-label';
249
+ label.innerText = '(you)'
250
+ label.style.color = '#777';
251
+ row.appendChild(label);
252
+ } else {
253
+ const micControl = document.createElement('button');
254
+ micControl.innerHTML = 'toggle mic'
255
+ micControl.style.margin = '0 4px';
256
+ micControl.onclick = () => {api.requestToggleAudio(user.id)}
257
+ row.appendChild(micControl);
258
+
259
+ const kickControl = document.createElement('button');
260
+ kickControl.innerHTML = 'kick'
261
+ kickControl.style.margin = '0 4px';
262
+ kickControl.onclick = () => {api.removeUser(user.id)}
263
+ row.appendChild(kickControl);
264
+
265
+ const allowBroadcastControl = document.createElement('button');
266
+ allowBroadcastControl.innerHTML = 'allow broadcast'
267
+ allowBroadcastControl.style.margin = '0 4px';
268
+ allowBroadcastControl.onclick = () => {api.allowBroadcast(user.id)}
269
+ subButtons.appendChild(allowBroadcastControl);
270
+
271
+ const disallowBroadcastControl = document.createElement('button');
272
+ disallowBroadcastControl.innerHTML = 'disallow broadcast'
273
+ disallowBroadcastControl.style.margin = '0 4px';
274
+ disallowBroadcastControl.onclick = () => {api.disallowBroadcast(user.id)}
275
+ subButtons.appendChild(disallowBroadcastControl);
276
+
277
+ const allowScreenshareControl = document.createElement('button');
278
+ allowScreenshareControl.innerHTML = 'allow screenshare'
279
+ allowScreenshareControl.style.margin = '0 4px';
280
+ allowScreenshareControl.onclick = () => {api.allowScreenshare(user.id)}
281
+ subButtons.appendChild(allowScreenshareControl);
282
+
283
+ const disallowScreenshareControl = document.createElement('button');
284
+ disallowScreenshareControl.innerHTML = 'disallow screenshare'
285
+ disallowScreenshareControl.style.margin = '0 4px';
286
+ disallowScreenshareControl.onclick = () => {api.disallowScreenshare(user.id)}
287
+ subButtons.appendChild(disallowScreenshareControl);
288
+ }
289
+
290
+ const raisedHandControl = document.createElement('button');
291
+ raisedHandControl.id = 'lower-hand-'+user.id;
292
+ raisedHandControl.innerHTML = 'lower hand'
293
+ raisedHandControl.style.margin = '0 4px';
294
+ raisedHandControl.style.display = 'none';
295
+ raisedHandControl.onclick = () => {api.lowerHand(user.id)}
296
+ row.appendChild(raisedHandControl);
297
+
298
+ participantList.appendChild(row);
299
+ participantList.appendChild(subButtons);
300
+
301
+ })
302
+ })
303
+
304
+ api.on('activeSpeakerChanged', ({data}) => {
305
+ const currentIndicator = document.querySelector('.speaker-indicator.show')
306
+ if(currentIndicator) {
307
+ currentIndicator.className = currentIndicator.className.replace(' show', '');
308
+
309
+ }
310
+
311
+ const newIndicator = document.querySelector(`.speaker-indicator-${data.user.id}`);
312
+ newIndicator.className = newIndicator.className + ' show'
313
+ })
314
+
110
315
  </script>
111
316
 
112
317
  </body>