bigbluebutton-html-plugin-sdk 0.0.94 → 0.0.96
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.
- package/README.md +1045 -21
- package/dist/cjs/core/api/BbbPluginSdk.js +13 -6
- package/dist/cjs/core/api/BbbPluginSdk.js.map +1 -1
- package/dist/cjs/core/api/types.d.ts +48 -1
- package/dist/cjs/core/auxiliary/plugin-information/locale-messages/types.d.ts +1 -1
- package/dist/cjs/data-channel/types.d.ts +2 -1
- package/dist/cjs/data-consumption/domain/meeting/index.d.ts +1 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/hooks.d.ts +3 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/hooks.js +9 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/hooks.js.map +1 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/types.d.ts +139 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/types.js +3 -0
- package/dist/cjs/data-consumption/domain/meeting/meeting-data/types.js.map +1 -0
- package/dist/cjs/data-consumption/domain/users/users-basic-info/types.d.ts +2 -0
- package/dist/cjs/data-consumption/enums.d.ts +1 -0
- package/dist/cjs/data-consumption/enums.js +1 -0
- package/dist/cjs/data-consumption/enums.js.map +1 -1
- package/dist/cjs/data-consumption/factory/hooks.d.ts +4 -0
- package/dist/cjs/data-consumption/factory/hooks.js +55 -0
- package/dist/cjs/data-consumption/factory/hooks.js.map +1 -0
- package/dist/cjs/data-consumption/factory/types.d.ts +3 -0
- package/dist/cjs/data-consumption/factory/types.js +3 -0
- package/dist/cjs/data-consumption/factory/types.js.map +1 -0
- package/dist/cjs/data-consumption/factory/utils.d.ts +2 -0
- package/dist/cjs/data-consumption/factory/utils.js +11 -0
- package/dist/cjs/data-consumption/factory/utils.js.map +1 -0
- package/dist/cjs/data-consumption/index.d.ts +1 -0
- package/dist/cjs/data-consumption/index.js +1 -0
- package/dist/cjs/data-consumption/index.js.map +1 -1
- package/dist/cjs/extensible-areas/base.d.ts +1 -0
- package/dist/cjs/extensible-areas/floating-window/component.d.ts +4 -1
- package/dist/cjs/extensible-areas/floating-window/component.js +4 -1
- package/dist/cjs/extensible-areas/floating-window/component.js.map +1 -1
- package/dist/cjs/extensible-areas/floating-window/types.d.ts +2 -0
- package/dist/cjs/extensible-areas/generic-content-item/component.d.ts +6 -2
- package/dist/cjs/extensible-areas/generic-content-item/component.js +6 -2
- package/dist/cjs/extensible-areas/generic-content-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/generic-content-item/types.d.ts +2 -0
- package/dist/cjs/extensible-areas/nav-bar-item/component.d.ts +6 -2
- package/dist/cjs/extensible-areas/nav-bar-item/component.js +6 -2
- package/dist/cjs/extensible-areas/nav-bar-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/nav-bar-item/types.d.ts +2 -0
- package/dist/cjs/extensible-areas/options-dropdown-item/component.d.ts +9 -2
- package/dist/cjs/extensible-areas/options-dropdown-item/component.js +8 -2
- package/dist/cjs/extensible-areas/options-dropdown-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/options-dropdown-item/types.d.ts +1 -0
- package/dist/cjs/extensible-areas/presentation-dropdown-item/component.d.ts +10 -3
- package/dist/cjs/extensible-areas/presentation-dropdown-item/component.js +9 -3
- package/dist/cjs/extensible-areas/presentation-dropdown-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/presentation-dropdown-item/types.d.ts +1 -0
- package/dist/cjs/extensible-areas/screenshare-helper-item/component.d.ts +3 -1
- package/dist/cjs/extensible-areas/screenshare-helper-item/component.js +3 -1
- package/dist/cjs/extensible-areas/screenshare-helper-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/screenshare-helper-item/types.d.ts +1 -0
- package/dist/cjs/extensible-areas/user-camera-dropdown-item/component.d.ts +7 -2
- package/dist/cjs/extensible-areas/user-camera-dropdown-item/component.js +7 -2
- package/dist/cjs/extensible-areas/user-camera-dropdown-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/user-camera-dropdown-item/types.d.ts +2 -0
- package/dist/cjs/extensible-areas/user-camera-helper-item/component.d.ts +3 -1
- package/dist/cjs/extensible-areas/user-camera-helper-item/component.js +3 -1
- package/dist/cjs/extensible-areas/user-camera-helper-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/user-camera-helper-item/types.d.ts +1 -0
- package/dist/cjs/extensible-areas/user-list-dropdown-item/component.d.ts +15 -5
- package/dist/cjs/extensible-areas/user-list-dropdown-item/component.js +15 -5
- package/dist/cjs/extensible-areas/user-list-dropdown-item/component.js.map +1 -1
- package/dist/cjs/extensible-areas/user-list-dropdown-item/types.d.ts +5 -0
- package/dist/cjs/extensible-areas/user-list-item-additional-information/component.d.ts +6 -2
- package/dist/cjs/extensible-areas/user-list-item-additional-information/component.js +6 -2
- package/dist/cjs/extensible-areas/user-list-item-additional-information/component.js.map +1 -1
- package/dist/cjs/extensible-areas/user-list-item-additional-information/types.d.ts +2 -0
- package/dist/cjs/learning-analytics-dashboard/commands.d.ts +5 -0
- package/dist/cjs/learning-analytics-dashboard/commands.js +43 -0
- package/dist/cjs/learning-analytics-dashboard/commands.js.map +1 -0
- package/dist/cjs/learning-analytics-dashboard/enums.d.ts +4 -1
- package/dist/cjs/learning-analytics-dashboard/enums.js +3 -0
- package/dist/cjs/learning-analytics-dashboard/enums.js.map +1 -1
- package/dist/cjs/learning-analytics-dashboard/types.d.ts +41 -2
- package/dist/cjs/server-commands/chat/commands.d.ts +18 -1
- package/dist/cjs/server-commands/chat/commands.js +25 -0
- package/dist/cjs/server-commands/chat/commands.js.map +1 -1
- package/dist/cjs/server-commands/chat/enum.d.ts +2 -1
- package/dist/cjs/server-commands/chat/enum.js +1 -0
- package/dist/cjs/server-commands/chat/enum.js.map +1 -1
- package/dist/cjs/server-commands/chat/types.d.ts +11 -0
- package/dist/cjs/server-commands/commands.d.ts +2 -0
- package/dist/cjs/ui-commands/chat/commands.d.ts +1 -1
- package/dist/cjs/ui-commands/chat/enums.d.ts +3 -0
- package/dist/cjs/ui-commands/chat/enums.js +8 -0
- package/dist/cjs/ui-commands/chat/enums.js.map +1 -0
- package/dist/cjs/ui-commands/chat/form/commands.d.ts +6 -3
- package/dist/cjs/ui-commands/chat/form/commands.js +13 -3
- package/dist/cjs/ui-commands/chat/form/commands.js.map +1 -1
- package/dist/cjs/ui-commands/chat/form/types.d.ts +4 -1
- package/dist/cjs/ui-commands/index.d.ts +1 -0
- package/dist/cjs/ui-commands/index.js +3 -1
- package/dist/cjs/ui-commands/index.js.map +1 -1
- package/dist/cjs/utils/logger/logger.d.ts +3 -1
- package/dist/cjs/utils/logger/logger.js +89 -1
- package/dist/cjs/utils/logger/logger.js.map +1 -1
- package/package.json +1 -1
- package/dist/cjs/learning-analytics-dashboard/hooks.d.ts +0 -2
- package/dist/cjs/learning-analytics-dashboard/hooks.js +0 -14
- package/dist/cjs/learning-analytics-dashboard/hooks.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ by the BigBlueButton HTML5 client to extend its functionalities.
|
|
|
8
8
|
|
|
9
9
|
An overview of the plugin architecture and capabilities can be found [here](https://github.com/bigbluebutton/plugins/blob/main/README.md#capabilities-and-technical-details).
|
|
10
10
|
|
|
11
|
+
For comprehensive documentation including architecture details, please refer to the [official BigBlueButton plugins documentation](https://docs.bigbluebutton.org/plugins/).
|
|
12
|
+
|
|
11
13
|
## Examples
|
|
12
14
|
|
|
13
15
|
A variety of example implementations to manipulate different parts of the BBB client can be found in the [`samples`](samples) folder.
|
|
@@ -115,21 +117,23 @@ In this case, the your manifest URL will be `https://<your-host>/plugins/sampleA
|
|
|
115
117
|
|
|
116
118
|
### Manifest Json
|
|
117
119
|
|
|
118
|
-
Here is
|
|
120
|
+
Here is a complete `manifest.json` example with all possible configurations:
|
|
119
121
|
|
|
120
122
|
```json
|
|
121
123
|
{
|
|
122
124
|
"requiredSdkVersion": "~0.0.59",
|
|
123
125
|
"name": "MyPlugin",
|
|
126
|
+
"version": "1.0.0",
|
|
124
127
|
"javascriptEntrypointUrl": "MyPlugin.js",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
128
|
+
"javascriptEntrypointIntegrity": "sha384-Bwsz2rxm...",
|
|
129
|
+
"localesBaseUrl": "https://cdn.domain.com/my-plugin/",
|
|
130
|
+
"dataChannels": [
|
|
127
131
|
{
|
|
128
132
|
"name": "public-channel",
|
|
129
133
|
"pushPermission": ["moderator","presenter"], // "moderator","presenter", "all"
|
|
130
134
|
"replaceOrDeletePermission": ["moderator", "creator"] // "moderator", "presenter","all", "creator"
|
|
131
135
|
}
|
|
132
|
-
],
|
|
136
|
+
],
|
|
133
137
|
"eventPersistence": {
|
|
134
138
|
"isEnabled": true, // By default it is not enabled
|
|
135
139
|
},
|
|
@@ -141,6 +145,9 @@ Here is as complete `manifest.json` example with all possible configurations:
|
|
|
141
145
|
"permissions": ["moderator", "viewer"]
|
|
142
146
|
}
|
|
143
147
|
],
|
|
148
|
+
"serverCommandsPermission": {
|
|
149
|
+
"chat.sendCustomPublicChatMessage": ["presenter", "moderator"]
|
|
150
|
+
},
|
|
144
151
|
"settingsSchema": [
|
|
145
152
|
{
|
|
146
153
|
"name": "myJson",
|
|
@@ -155,6 +162,39 @@ Here is as complete `manifest.json` example with all possible configurations:
|
|
|
155
162
|
}
|
|
156
163
|
```
|
|
157
164
|
|
|
165
|
+
**Manifest field descriptions:**
|
|
166
|
+
|
|
167
|
+
| Field | Required | Description |
|
|
168
|
+
|-------|----------|-------------|
|
|
169
|
+
| `requiredSdkVersion` | Yes | Specifies the SDK version compatibility (e.g., `~0.0.59`) |
|
|
170
|
+
| `name` | Yes | Plugin name as referenced in configuration |
|
|
171
|
+
| `version` | No | Plugin version for cache-busting (appends `?version=X` to JS URL) |
|
|
172
|
+
| `javascriptEntrypointUrl` | Yes | URL to the main JavaScript bundle |
|
|
173
|
+
| `javascriptEntrypointIntegrity` | No | Subresource Integrity (SRI) hash for security |
|
|
174
|
+
| `localesBaseUrl` | No | Base URL for internationalization locale files |
|
|
175
|
+
| `dataChannels` | No | Array of data channels for inter-client communication |
|
|
176
|
+
| `eventPersistence` | No | Configuration for persisting events to meeting recording |
|
|
177
|
+
| `remoteDataSources` | No | External API endpoints for fetching data |
|
|
178
|
+
| `serverCommandsPermission` | No | Role-based permissions for server commands |
|
|
179
|
+
| `settingsSchema` | No | Configuration schema for plugin settings |
|
|
180
|
+
|
|
181
|
+
**Possible values for permissions:**
|
|
182
|
+
- `pushPermission`: `"moderator"`, `"presenter"`, `"all"`, `"viewer"`
|
|
183
|
+
- `replaceOrDeletePermission`: `"moderator"`, `"presenter"`, `"all"`, `"viewer"`, `"creator"`
|
|
184
|
+
- `serverCommandsPermission`: `"moderator"`, `"presenter"`, `"all"`, `"viewer"`
|
|
185
|
+
- `remoteDataSources.permissions`: `"moderator"`, `"viewer"`, `"presenter"`
|
|
186
|
+
|
|
187
|
+
**Possible values for fetchMode:**
|
|
188
|
+
- `"onMeetingCreate"`: Fetch once when meeting is created and cache the result
|
|
189
|
+
- `"onDemand"`: Fetch every time the plugin requests the data
|
|
190
|
+
|
|
191
|
+
**Possible values for settingsSchema.type:**
|
|
192
|
+
- `"int"`: Integer number
|
|
193
|
+
- `"float"`: Floating-point number
|
|
194
|
+
- `"string"`: Text string
|
|
195
|
+
- `"boolean"`: True/false value
|
|
196
|
+
- `"json"`: JSON object
|
|
197
|
+
|
|
158
198
|
To better understand remote-data-sources, please, refer to [this section](#external-data-resources)
|
|
159
199
|
|
|
160
200
|
**settingsSchema:**
|
|
@@ -227,12 +267,229 @@ That being said, here are the extensible areas we have so far:
|
|
|
227
267
|
|
|
228
268
|
Mind that no plugin will interfere into another's extensible area. So feel free to set whatever you need into a certain plugin with no worries.
|
|
229
269
|
|
|
230
|
-
|
|
270
|
+
**Example usage for different extensible areas:**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
274
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
// 1. Action Bar Items (bottom toolbar)
|
|
278
|
+
pluginApi.setActionsBarItems([
|
|
279
|
+
new ActionsBarButton({
|
|
280
|
+
icon: { iconName: 'user' },
|
|
281
|
+
tooltip: 'My custom action',
|
|
282
|
+
onClick: () => pluginLogger.info('Action bar button clicked'),
|
|
283
|
+
position: ActionsBarPosition.RIGHT,
|
|
284
|
+
}),
|
|
285
|
+
new ActionsBarSeparator({
|
|
286
|
+
position: ActionsBarPosition.RIGHT,
|
|
287
|
+
}),
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
// 2. Action Button Dropdown Items (three dots menu in action bar)
|
|
291
|
+
pluginApi.setActionButtonDropdownItems([
|
|
292
|
+
new ActionButtonDropdownSeparator(),
|
|
293
|
+
new ActionButtonDropdownOption({
|
|
294
|
+
label: 'Custom dropdown option',
|
|
295
|
+
icon: 'copy',
|
|
296
|
+
tooltip: 'This is a custom option',
|
|
297
|
+
onClick: () => pluginLogger.info('Dropdown option clicked'),
|
|
298
|
+
}),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// 3. Options Dropdown Items (three dots menu in top-right)
|
|
302
|
+
pluginApi.setOptionsDropdownItems([
|
|
303
|
+
new OptionsDropdownSeparator(),
|
|
304
|
+
new OptionsDropdownOption({
|
|
305
|
+
label: 'Plugin Settings',
|
|
306
|
+
icon: 'settings',
|
|
307
|
+
onClick: () => pluginLogger.info('Options clicked'),
|
|
308
|
+
}),
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
// 4. Nav Bar Items (top navigation bar)
|
|
312
|
+
pluginApi.setNavBarItems([
|
|
313
|
+
new NavBarButton({
|
|
314
|
+
icon: 'user',
|
|
315
|
+
label: 'Custom Nav Button',
|
|
316
|
+
tooltip: 'Navigate to custom area',
|
|
317
|
+
position: NavBarItemPosition.RIGHT,
|
|
318
|
+
onClick: () => pluginLogger.info('Nav button clicked'),
|
|
319
|
+
hasSeparator: true,
|
|
320
|
+
}),
|
|
321
|
+
new NavBarInfo({
|
|
322
|
+
label: 'Plugin Active',
|
|
323
|
+
position: NavBarItemPosition.CENTER,
|
|
324
|
+
hasSeparator: false,
|
|
325
|
+
}),
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
// 5. Presentation Toolbar Items (presentation controls)
|
|
329
|
+
pluginApi.setPresentationToolbarItems([
|
|
330
|
+
new PresentationToolbarButton({
|
|
331
|
+
label: 'Custom Tool',
|
|
332
|
+
tooltip: 'Use custom presentation tool',
|
|
333
|
+
onClick: () => pluginLogger.info('Presentation tool clicked'),
|
|
334
|
+
}),
|
|
335
|
+
new PresentationToolbarSeparator(),
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
// 6. User List Dropdown Items (per-user menu)
|
|
339
|
+
pluginApi.setUserListDropdownItems([
|
|
340
|
+
new UserListDropdownSeparator(),
|
|
341
|
+
new UserListDropdownOption({
|
|
342
|
+
label: 'View Profile',
|
|
343
|
+
icon: 'user',
|
|
344
|
+
onClick: (userId) => {
|
|
345
|
+
pluginLogger.info('View profile for user:', userId);
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
// 7. Audio Settings Dropdown Items
|
|
351
|
+
pluginApi.setAudioSettingsDropdownItems([
|
|
352
|
+
new AudioSettingsDropdownOption({
|
|
353
|
+
label: 'Audio Plugin Settings',
|
|
354
|
+
icon: 'settings',
|
|
355
|
+
onClick: () => pluginLogger.info('Audio settings clicked'),
|
|
356
|
+
}),
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
// 8. Camera Settings Dropdown Items
|
|
360
|
+
pluginApi.setCameraSettingsDropdownItems([
|
|
361
|
+
new CameraSettingsDropdownOption({
|
|
362
|
+
label: 'Camera Plugin Settings',
|
|
363
|
+
icon: 'video',
|
|
364
|
+
onClick: () => pluginLogger.info('Camera settings clicked'),
|
|
365
|
+
}),
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
// 9. Floating Window
|
|
369
|
+
pluginApi.setFloatingWindows([
|
|
370
|
+
new FloatingWindow({
|
|
371
|
+
top: 100,
|
|
372
|
+
left: 100,
|
|
373
|
+
width: 400,
|
|
374
|
+
height: 300,
|
|
375
|
+
movable: true,
|
|
376
|
+
resizable: true,
|
|
377
|
+
backgroundColor: '#ffffff',
|
|
378
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
379
|
+
contentFunction: (element: HTMLElement) => {
|
|
380
|
+
const root = ReactDOM.createRoot(element);
|
|
381
|
+
root.render(
|
|
382
|
+
<div style={{ padding: '20px' }}>
|
|
383
|
+
<h2>Custom Floating Window</h2>
|
|
384
|
+
<p>This is custom plugin content!</p>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
return root;
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
// 10. Generic Content Sidekick Area
|
|
393
|
+
pluginApi.setGenericContentItems([
|
|
394
|
+
new GenericContentSidekickArea({
|
|
395
|
+
id: 'my-plugin-content',
|
|
396
|
+
name: 'Plugin Content',
|
|
397
|
+
section: 'Custom Section',
|
|
398
|
+
buttonIcon: 'copy',
|
|
399
|
+
open: false,
|
|
400
|
+
contentFunction: (element: HTMLElement) => {
|
|
401
|
+
const root = ReactDOM.createRoot(element);
|
|
402
|
+
root.render(
|
|
403
|
+
<div style={{ padding: '20px' }}>
|
|
404
|
+
<h1>Sidekick Content</h1>
|
|
405
|
+
<p>Custom sidekick panel content here!</p>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
return root;
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
// 11. User List Item Additional Information
|
|
414
|
+
pluginApi.setUserListItemAdditionalInformation([
|
|
415
|
+
new UserListItemAdditionalInformation({
|
|
416
|
+
userId: 'specific-user-id', // or use a function to determine dynamically
|
|
417
|
+
label: 'VIP',
|
|
418
|
+
icon: 'star',
|
|
419
|
+
}),
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
// 12. Presentation Dropdown Items
|
|
423
|
+
pluginApi.setPresentationDropdownItems([
|
|
424
|
+
new PresentationDropdownOption({
|
|
425
|
+
label: 'Export Presentation',
|
|
426
|
+
icon: 'download',
|
|
427
|
+
onClick: () => pluginLogger.info('Export presentation clicked'),
|
|
428
|
+
}),
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
// 13. User Camera Dropdown Items
|
|
432
|
+
pluginApi.setUserCameraDropdownItems([
|
|
433
|
+
new UserCameraDropdownOption({
|
|
434
|
+
label: 'Camera Effects',
|
|
435
|
+
icon: 'video',
|
|
436
|
+
displayFunction: ({ userId }) => {
|
|
437
|
+
// Show only for specific users or conditions
|
|
438
|
+
return true;
|
|
439
|
+
},
|
|
440
|
+
onClick: ({ userId, streamId }) => {
|
|
441
|
+
pluginLogger.info('Camera option clicked:', userId, streamId);
|
|
442
|
+
},
|
|
443
|
+
}),
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
}, []);
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Key Points:**
|
|
451
|
+
- Each extensible area has its own setter function (e.g., `setActionsBarItems`, `setOptionsDropdownItems`)
|
|
452
|
+
- Items are set as arrays, allowing multiple items per area
|
|
453
|
+
- Use separators to visually group related items
|
|
454
|
+
- Items from different plugins won't conflict - each plugin manages its own items
|
|
455
|
+
- Most items support icons, labels, tooltips, and onClick handlers
|
|
456
|
+
- Some items support positioning (LEFT, CENTER, RIGHT) or display conditions
|
|
457
|
+
|
|
458
|
+
### Auxiliary functions
|
|
231
459
|
|
|
232
460
|
- `getSessionToken`: returns the user session token located on the user's URL.
|
|
233
461
|
- `getJoinUrl`: returns the join url associated with the parameters passed as an argument. Since it fetches the BigBlueButton API, this getter method is asynchronous.
|
|
234
462
|
- `useLocaleMessages`: returns the messages to be used in internationalization functions (recommend to use `react-intl`, as example, refer to official plugins)
|
|
235
463
|
|
|
464
|
+
**Example usage:**
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
468
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
469
|
+
|
|
470
|
+
// Get session token
|
|
471
|
+
const sessionToken = pluginApi.getSessionToken();
|
|
472
|
+
console.log('Session token:', sessionToken);
|
|
473
|
+
|
|
474
|
+
// Get join URL (async)
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
pluginApi.getJoinUrl({
|
|
477
|
+
fullName: 'John Doe',
|
|
478
|
+
role: 'MODERATOR',
|
|
479
|
+
// other join parameters...
|
|
480
|
+
}).then((joinUrl) => {
|
|
481
|
+
console.log('Join URL:', joinUrl);
|
|
482
|
+
});
|
|
483
|
+
}, []);
|
|
484
|
+
|
|
485
|
+
// Use locale messages for internationalization
|
|
486
|
+
const localeMessages = pluginApi.useLocaleMessages();
|
|
487
|
+
console.log('Current locale:', localeMessages);
|
|
488
|
+
|
|
489
|
+
return null;
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
|
|
236
493
|
### Realtime data consumption
|
|
237
494
|
|
|
238
495
|
- `useCurrentPresentation` hook: provides information regarding the current presentation;
|
|
@@ -257,6 +514,87 @@ export interface GraphqlResponseWrapper<TData> {
|
|
|
257
514
|
|
|
258
515
|
So we have the `data`, which is different for each hook, that's why it's a generic, the error, that will be set if, and only if, there is an error, otherwise it is undefined, and loading, which tells the developer if the query is still loading (being fetched) or not.
|
|
259
516
|
|
|
517
|
+
**Example usage:**
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
521
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
522
|
+
|
|
523
|
+
// Get current user information
|
|
524
|
+
const { data: currentUser, loading: userLoading, error: userError } =
|
|
525
|
+
pluginApi.useCurrentUser();
|
|
526
|
+
|
|
527
|
+
// Get meeting information
|
|
528
|
+
const { data: meeting, loading: meetingLoading } = pluginApi.useMeeting();
|
|
529
|
+
|
|
530
|
+
// Get current presentation
|
|
531
|
+
const { data: presentation } = pluginApi.useCurrentPresentation();
|
|
532
|
+
|
|
533
|
+
// Get all users basic info
|
|
534
|
+
const { data: usersBasicInfo } = pluginApi.useUsersBasicInfo();
|
|
535
|
+
|
|
536
|
+
// Get loaded chat messages
|
|
537
|
+
const { data: chatMessages } = pluginApi.useLoadedChatMessages();
|
|
538
|
+
|
|
539
|
+
// Get talking indicators
|
|
540
|
+
const { data: talkingIndicators } = pluginApi.useTalkingIndicator();
|
|
541
|
+
|
|
542
|
+
// Get plugin settings
|
|
543
|
+
const { data: pluginSettings } = pluginApi.usePluginSettings();
|
|
544
|
+
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (currentUser && !userLoading) {
|
|
547
|
+
pluginLogger.info('Current user:', currentUser);
|
|
548
|
+
pluginLogger.info('User is presenter:', currentUser.presenter);
|
|
549
|
+
pluginLogger.info('User is moderator:', currentUser.isModerator);
|
|
550
|
+
}
|
|
551
|
+
if (userError) {
|
|
552
|
+
pluginLogger.error('Error fetching user:', userError);
|
|
553
|
+
}
|
|
554
|
+
}, [currentUser, userLoading, userError]);
|
|
555
|
+
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (meeting) {
|
|
558
|
+
pluginLogger.info('Meeting ID:', meeting.meetingId);
|
|
559
|
+
pluginLogger.info('Meeting name:', meeting.name);
|
|
560
|
+
}
|
|
561
|
+
}, [meeting]);
|
|
562
|
+
|
|
563
|
+
useEffect(() => {
|
|
564
|
+
if (presentation) {
|
|
565
|
+
pluginLogger.info('Current page:', presentation.currentPage?.num);
|
|
566
|
+
pluginLogger.info('Total pages:', presentation.totalPages);
|
|
567
|
+
}
|
|
568
|
+
}, [presentation]);
|
|
569
|
+
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (chatMessages) {
|
|
572
|
+
pluginLogger.info('Total messages:', chatMessages.length);
|
|
573
|
+
chatMessages.forEach((msg) => {
|
|
574
|
+
pluginLogger.info(`Message from ${msg.senderName}: ${msg.message}`);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}, [chatMessages]);
|
|
578
|
+
|
|
579
|
+
// Custom subscription example
|
|
580
|
+
const { data: customData } = pluginApi.useCustomSubscription<CustomDataType>(`
|
|
581
|
+
subscription MyCustomSubscription {
|
|
582
|
+
user {
|
|
583
|
+
userId
|
|
584
|
+
name
|
|
585
|
+
role
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
`);
|
|
589
|
+
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
if (customData) {
|
|
592
|
+
pluginLogger.info('Custom subscription data:', customData);
|
|
593
|
+
}
|
|
594
|
+
}, [customData]);
|
|
595
|
+
|
|
596
|
+
```
|
|
597
|
+
|
|
260
598
|
### Realtime Data Creation
|
|
261
599
|
|
|
262
600
|
**`useCustomMutation` Hook**
|
|
@@ -350,7 +688,7 @@ The data-channel name must be in the `manifest.json` along with all the permissi
|
|
|
350
688
|
{
|
|
351
689
|
"name": "channel-name",
|
|
352
690
|
"pushPermission": ["moderator","presenter"],
|
|
353
|
-
"replaceOrDeletePermission": ["moderator", "
|
|
691
|
+
"replaceOrDeletePermission": ["moderator", "creator"]
|
|
354
692
|
}
|
|
355
693
|
]
|
|
356
694
|
}
|
|
@@ -421,7 +759,7 @@ One other thing is that the type of the return is precisely the same type requir
|
|
|
421
759
|
- volume:
|
|
422
760
|
- set: this function will set the external video volume to a certain number between 0 and 1 (that is 0% and);
|
|
423
761
|
- layout:
|
|
424
|
-
- changeEnforcedLayout: (deprecated) Changes the enforced layout
|
|
762
|
+
- changeEnforcedLayout: (deprecated) Changes the enforced layout
|
|
425
763
|
- setEnforcedLayout: Sets the enforced layout
|
|
426
764
|
- navBar:
|
|
427
765
|
- setDisplayNavBar: Sets the displayNavBar to true (show it) or false (hide it).
|
|
@@ -444,23 +782,71 @@ One other thing is that the type of the return is precisely the same type requir
|
|
|
444
782
|
- close: this function will close the sidekick options panel automatically (and also the sidebar content if open, to avoid inconsistencies in ui);
|
|
445
783
|
- user-status:
|
|
446
784
|
- setAwayStatus: this function will set the away status of the user to a certain status;
|
|
447
|
-
See usage ahead:
|
|
448
785
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
786
|
+
**Example usage:**
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
790
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
791
|
+
|
|
792
|
+
useEffect(() => {
|
|
793
|
+
// Open and fill chat form
|
|
794
|
+
pluginApi.uiCommands.chat.form.open();
|
|
795
|
+
pluginApi.uiCommands.chat.form.fill({
|
|
796
|
+
text: 'Just an example message filled by the plugin',
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Set speaker volume to 50%
|
|
800
|
+
pluginApi.uiCommands.conference.setSpeakerLevel(0.5);
|
|
801
|
+
|
|
802
|
+
// Set external video volume to 75%
|
|
803
|
+
pluginApi.uiCommands.externalVideo.volume.set(0.75);
|
|
804
|
+
|
|
805
|
+
// Change layout
|
|
806
|
+
pluginApi.uiCommands.layout.setEnforcedLayout('FOCUS');
|
|
807
|
+
|
|
808
|
+
// Show/hide nav bar
|
|
809
|
+
pluginApi.uiCommands.navBar.setDisplayNavBar(false);
|
|
810
|
+
|
|
811
|
+
// Send a notification
|
|
812
|
+
pluginApi.uiCommands.notification.send({
|
|
813
|
+
type: 'info',
|
|
814
|
+
message: 'This is a notification from the plugin',
|
|
815
|
+
icon: 'user',
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Open presentation area
|
|
819
|
+
pluginApi.uiCommands.presentationArea.open();
|
|
820
|
+
|
|
821
|
+
// Sidekick area commands
|
|
822
|
+
pluginApi.uiCommands.sidekickArea.options.setMenuBadge('my-content-id', '5');
|
|
823
|
+
pluginApi.uiCommands.sidekickArea.options.renameGenericContentMenu(
|
|
824
|
+
'my-content-id',
|
|
825
|
+
'New Menu Name'
|
|
826
|
+
);
|
|
827
|
+
pluginApi.uiCommands.sidekickArea.options.renameGenericContentSection(
|
|
828
|
+
'my-content-id',
|
|
829
|
+
'New Section Name'
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
// Camera commands
|
|
833
|
+
pluginApi.uiCommands.camera.setSelfViewDisableAllDevices(true);
|
|
834
|
+
pluginApi.uiCommands.camera.setSelfViewDisable('camera-stream-id', false);
|
|
835
|
+
|
|
836
|
+
// Set away status
|
|
837
|
+
pluginApi.uiCommands.userStatus.setAwayStatus(true);
|
|
838
|
+
}, []);
|
|
839
|
+
|
|
454
840
|
```
|
|
455
841
|
|
|
456
842
|
So the idea is that we have a `uiCommands` object and at a point, there will be the command to do the intended action, such as open the chat form and/or fill it, as demonstrated above
|
|
457
843
|
|
|
458
844
|
### Server Commands
|
|
459
|
-
|
|
845
|
+
|
|
460
846
|
`serverCommands` object: It contains all the possible commands available to the developer to interact with the BBB core server, see the ones implemented down below:
|
|
461
|
-
|
|
847
|
+
|
|
462
848
|
- chat:
|
|
463
|
-
-
|
|
849
|
+
- sendPublicChatMessage: This function sends a message to the public chat on behalf of the currently logged-in user.
|
|
464
850
|
|
|
465
851
|
- sendCustomPublicMessage: This function sends a text message to the public chat, optionally including custom metadata.
|
|
466
852
|
> **Note**: The custom messages sent by plugins are not automatically rendered by the client. To display these messages, a plugin must handle the rendering using `useLoadedChatMessages` and `useChatMessageDomElements`.
|
|
@@ -469,31 +855,335 @@ So the idea is that we have a `uiCommands` object and at a point, there will be
|
|
|
469
855
|
- save: this function saves the given text, locale and caption type
|
|
470
856
|
- addLocale: this function sends a locale to be added to the available options
|
|
471
857
|
|
|
858
|
+
**Example usage:**
|
|
859
|
+
|
|
860
|
+
```typescript
|
|
861
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
862
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
863
|
+
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
// Add a button that sends a chat message
|
|
866
|
+
pluginApi.setActionButtonDropdownItems([
|
|
867
|
+
new ActionButtonDropdownOption({
|
|
868
|
+
label: 'Send chat message',
|
|
869
|
+
icon: 'chat',
|
|
870
|
+
onClick: () => {
|
|
871
|
+
// Send a public chat message
|
|
872
|
+
pluginApi.serverCommands.chat.sendPublicChatMessage({
|
|
873
|
+
textMessageInMarkdownFormat: 'Hello from plugin!',
|
|
874
|
+
pluginCustomMetadata: pluginUuid,
|
|
875
|
+
});
|
|
876
|
+
},
|
|
877
|
+
}),
|
|
878
|
+
new ActionButtonDropdownOption({
|
|
879
|
+
label: 'Send custom message',
|
|
880
|
+
icon: 'copy',
|
|
881
|
+
onClick: () => {
|
|
882
|
+
// Send a custom public chat message with metadata
|
|
883
|
+
pluginApi.serverCommands.chat.sendCustomPublicMessage({
|
|
884
|
+
textMessageInMarkdownFormat: '**Custom message** with metadata',
|
|
885
|
+
pluginCustomMetadata: {
|
|
886
|
+
type: 'notification',
|
|
887
|
+
pluginId: pluginUuid,
|
|
888
|
+
timestamp: Date.now(),
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
},
|
|
892
|
+
}),
|
|
893
|
+
new ActionButtonDropdownOption({
|
|
894
|
+
label: 'Save caption',
|
|
895
|
+
icon: 'closed_caption',
|
|
896
|
+
onClick: () => {
|
|
897
|
+
// Save a caption
|
|
898
|
+
pluginApi.serverCommands.caption.save({
|
|
899
|
+
text: 'This is a caption text',
|
|
900
|
+
locale: 'en-US',
|
|
901
|
+
captionType: 'TYPED',
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
}),
|
|
905
|
+
new ActionButtonDropdownOption({
|
|
906
|
+
label: 'Add caption locale',
|
|
907
|
+
icon: 'closed_caption',
|
|
908
|
+
onClick: () => {
|
|
909
|
+
// Add a new locale option for captions
|
|
910
|
+
pluginApi.serverCommands.caption.addLocale({
|
|
911
|
+
locale: 'pt-BR',
|
|
912
|
+
localeName: 'Portuguese (Brazil)',
|
|
913
|
+
});
|
|
914
|
+
},
|
|
915
|
+
}),
|
|
916
|
+
]);
|
|
917
|
+
}, []);
|
|
918
|
+
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Note on permissions:** Some server commands have permission controls based on roles defined in the manifest. See the [Manifest Json](#manifest-json) section for configuration details.
|
|
922
|
+
|
|
472
923
|
### Dom Element Manipulation
|
|
473
924
|
|
|
474
925
|
- `useChatMessageDomElements` hook: This hook will return the dom element of a chat message reactively, so one can modify whatever is inside, such as text, css, js, etc.;
|
|
475
926
|
- `useUserCameraDomElements` hook: This hook will return the dom element of each of the user's webcam corresponding to the streamIds passed reactively, so one can modify whatever is inside, such as text, css, js, etc., and also can get the video element within it;
|
|
476
927
|
|
|
928
|
+
**Example usage:**
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
932
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
933
|
+
const [chatIdsToHighlight, setChatIdsToHighlight] = useState<string[]>([]);
|
|
934
|
+
const [streamIdsToStyle, setStreamIdsToStyle] = useState<string[]>([]);
|
|
935
|
+
|
|
936
|
+
// Example 1: Manipulate chat messages (highlight mentions)
|
|
937
|
+
const { data: chatMessages } = pluginApi.useLoadedChatMessages();
|
|
938
|
+
|
|
939
|
+
useEffect(() => {
|
|
940
|
+
if (chatMessages) {
|
|
941
|
+
// Filter messages that mention someone with @ pattern
|
|
942
|
+
const MENTION_REGEX = /@([A-Z][a-z]+ ){0,2}[A-Z][a-z]+/;
|
|
943
|
+
const messageIds = chatMessages
|
|
944
|
+
.filter((msg) => MENTION_REGEX.test(msg.message))
|
|
945
|
+
.map((msg) => msg.messageId);
|
|
946
|
+
setChatIdsToHighlight(messageIds);
|
|
947
|
+
}
|
|
948
|
+
}, [chatMessages]);
|
|
949
|
+
|
|
950
|
+
// Get DOM elements for the chat messages we want to manipulate
|
|
951
|
+
const chatMessagesDomElements = pluginApi.useChatMessageDomElements(chatIdsToHighlight);
|
|
952
|
+
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
if (chatMessagesDomElements && chatMessagesDomElements.length > 0) {
|
|
955
|
+
chatMessagesDomElements.forEach((chatMessageDomElement) => {
|
|
956
|
+
const MENTION_REGEX = /@([A-Z][a-z]+ ){0,2}[A-Z][a-z]+/;
|
|
957
|
+
const mention = chatMessageDomElement.innerHTML.match(MENTION_REGEX);
|
|
958
|
+
|
|
959
|
+
if (mention) {
|
|
960
|
+
// Highlight the mention with custom styling
|
|
961
|
+
chatMessageDomElement.innerHTML = chatMessageDomElement.innerHTML.replace(
|
|
962
|
+
mention[0],
|
|
963
|
+
`<span style="color: #4185cf; background-color: #f2f6f8; padding: 2px 4px; border-radius: 3px;">${mention[0]}</span>`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Style the parent element
|
|
968
|
+
const { parentElement } = chatMessageDomElement;
|
|
969
|
+
if (parentElement) {
|
|
970
|
+
parentElement.style.paddingTop = '0.5rem';
|
|
971
|
+
parentElement.style.paddingBottom = '0.5rem';
|
|
972
|
+
parentElement.style.borderLeft = '3px solid #4185cf';
|
|
973
|
+
parentElement.style.backgroundColor = '#f8f9fa';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
pluginLogger.info('Styled chat message:', chatMessageDomElement);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}, [chatMessagesDomElements]);
|
|
980
|
+
|
|
981
|
+
// Example 2: Manipulate user camera streams
|
|
982
|
+
const { data: videoStreams } = pluginApi.useCustomSubscription<VideoStreamsType>(`
|
|
983
|
+
subscription VideoStreams {
|
|
984
|
+
user_camera {
|
|
985
|
+
streamId
|
|
986
|
+
user {
|
|
987
|
+
userId
|
|
988
|
+
name
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
`);
|
|
993
|
+
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
if (videoStreams?.user_camera) {
|
|
996
|
+
// Get all stream IDs
|
|
997
|
+
const streamIds = videoStreams.user_camera.map((stream) => stream.streamId);
|
|
998
|
+
setStreamIdsToStyle(streamIds);
|
|
999
|
+
}
|
|
1000
|
+
}, [videoStreams]);
|
|
1001
|
+
|
|
1002
|
+
// Get DOM elements for user cameras
|
|
1003
|
+
const userCameraDomElements = pluginApi.useUserCameraDomElements(streamIdsToStyle);
|
|
1004
|
+
|
|
1005
|
+
useEffect(() => {
|
|
1006
|
+
if (userCameraDomElements && userCameraDomElements.length > 0) {
|
|
1007
|
+
userCameraDomElements.forEach((cameraElement) => {
|
|
1008
|
+
// Add a custom border to the camera
|
|
1009
|
+
cameraElement.element.style.border = '3px solid #0c5cb3';
|
|
1010
|
+
cameraElement.element.style.borderRadius = '8px';
|
|
1011
|
+
cameraElement.element.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
|
|
1012
|
+
|
|
1013
|
+
// Access the video element directly
|
|
1014
|
+
if (cameraElement.videoElement) {
|
|
1015
|
+
cameraElement.videoElement.style.objectFit = 'cover';
|
|
1016
|
+
pluginLogger.info('Styled video element for stream:', cameraElement.streamId);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Add a custom overlay
|
|
1020
|
+
const overlay = document.createElement('div');
|
|
1021
|
+
overlay.style.position = 'absolute';
|
|
1022
|
+
overlay.style.top = '5px';
|
|
1023
|
+
overlay.style.right = '5px';
|
|
1024
|
+
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
|
1025
|
+
overlay.style.color = 'white';
|
|
1026
|
+
overlay.style.padding = '4px 8px';
|
|
1027
|
+
overlay.style.borderRadius = '4px';
|
|
1028
|
+
overlay.style.fontSize = '12px';
|
|
1029
|
+
overlay.textContent = '🎥 Live';
|
|
1030
|
+
cameraElement.element.style.position = 'relative';
|
|
1031
|
+
cameraElement.element.appendChild(overlay);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}, [userCameraDomElements]);
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
**Important notes:**
|
|
1038
|
+
- Use DOM manipulation carefully and only when necessary
|
|
1039
|
+
- Always check if elements exist before manipulating them
|
|
1040
|
+
- Prefer using the provided hooks over direct DOM queries
|
|
1041
|
+
- Be aware that excessive DOM manipulation can impact performance
|
|
1042
|
+
- When styling chat messages, avoid changing the actual text content as it may cause issues with recordings for more information on that, see [guidelines section](https://docs.bigbluebutton.org/plugins/#dom-element-manipulation-guidelines)
|
|
1043
|
+
|
|
477
1044
|
### Learning Analytics Dashboard integration
|
|
478
1045
|
|
|
479
|
-
|
|
1046
|
+
This integration allow you to insert/update entry in LAD (Learning Analytics Dashboard) via `upsertUserData` function and also delete entries via `deleteUserData` function.
|
|
1047
|
+
|
|
1048
|
+
It's an object available in the `pluginApi` that wraps those 3 functions:
|
|
1049
|
+
|
|
1050
|
+
- `pluginApi.learningAnalyticsDashboard.upsertUserData`
|
|
1051
|
+
- `pluginApi.learningAnalyticsDashboard.deleteUserData`
|
|
1052
|
+
- `pluginApi.learningAnalyticsDashboard.clearAllUsersData`
|
|
480
1053
|
|
|
481
|
-
|
|
1054
|
+
For the `upsert` function, the argument's data object structure must be:
|
|
482
1055
|
|
|
483
1056
|
```ts
|
|
484
|
-
interface
|
|
485
|
-
cardTitle: string;
|
|
1057
|
+
interface LearningAnalyticsDashboardUserData {
|
|
1058
|
+
cardTitle: string;
|
|
486
1059
|
columnTitle: string;
|
|
487
1060
|
value: string;
|
|
488
1061
|
}
|
|
489
1062
|
```
|
|
490
1063
|
|
|
1064
|
+
For the `deleteUserData` function, the argument's data object structure must be:
|
|
1065
|
+
|
|
1066
|
+
```ts
|
|
1067
|
+
interface LearningAnalyticsDashboardDeleteUserData {
|
|
1068
|
+
cardTitle: string;
|
|
1069
|
+
columnTitle: string;
|
|
1070
|
+
}
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
For the `clearAllUsersData` function, the argument is the cardTitle (optionally), and when it's not sent, all the entries for a specific plugin will be deleted. (And if the card ends up empty, it will be removed)
|
|
1074
|
+
|
|
1075
|
+
If the user is a moderator, there is the possibility to publish data on behalf of other users by using the second **optional** parameter named `targetUserId`
|
|
1076
|
+
|
|
491
1077
|
So that the data will appear in the following form:
|
|
492
1078
|
|
|
493
1079
|
| User | Count | `<columnTitle>` |
|
|
494
1080
|
| --- | :-- | --: |
|
|
495
1081
|
| user-name | 1 | `<value>` |
|
|
496
1082
|
|
|
1083
|
+
**Example usage:**
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1087
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1088
|
+
const [interactionCount, setInteractionCount] = useState(0);
|
|
1089
|
+
|
|
1090
|
+
// Example: Track user interactions and send to Learning Analytics Dashboard
|
|
1091
|
+
useEffect(() => {
|
|
1092
|
+
const handleUserInteraction = () => {
|
|
1093
|
+
const newCount = interactionCount + 1;
|
|
1094
|
+
setInteractionCount(newCount);
|
|
1095
|
+
|
|
1096
|
+
// Send data to Learning Analytics Dashboard
|
|
1097
|
+
pluginApi.sendGenericDataForLearningAnalyticsDashboard({
|
|
1098
|
+
cardTitle: 'User Interactions', // For future use
|
|
1099
|
+
columnTitle: 'Total Clicks',
|
|
1100
|
+
value: newCount.toString(),
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
pluginLogger.info('Sent interaction data to dashboard:', newCount);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// Example: Track button clicks
|
|
1107
|
+
pluginApi.setActionButtonDropdownItems([
|
|
1108
|
+
new ActionButtonDropdownOption({
|
|
1109
|
+
label: 'Track Interaction',
|
|
1110
|
+
icon: 'user',
|
|
1111
|
+
onClick: handleUserInteraction,
|
|
1112
|
+
}),
|
|
1113
|
+
]);
|
|
1114
|
+
}, [interactionCount]);
|
|
1115
|
+
|
|
1116
|
+
// Example: Send engagement metrics periodically
|
|
1117
|
+
useEffect(() => {
|
|
1118
|
+
const interval = setInterval(() => {
|
|
1119
|
+
const engagementScore = Math.floor(Math.random() * 100);
|
|
1120
|
+
|
|
1121
|
+
pluginApi.sendGenericDataForLearningAnalyticsDashboard({
|
|
1122
|
+
cardTitle: 'Engagement Metrics',
|
|
1123
|
+
columnTitle: 'Engagement Score',
|
|
1124
|
+
value: `${engagementScore}%`,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
pluginLogger.info('Sent engagement score:', engagementScore);
|
|
1128
|
+
}, 60000); // Every minute
|
|
1129
|
+
|
|
1130
|
+
return () => clearInterval(interval);
|
|
1131
|
+
}, []);
|
|
1132
|
+
|
|
1133
|
+
// Example: Track specific plugin events
|
|
1134
|
+
const { data: chatMessages } = pluginApi.useLoadedChatMessages();
|
|
1135
|
+
|
|
1136
|
+
useEffect(() => {
|
|
1137
|
+
if (chatMessages) {
|
|
1138
|
+
const messageCount = chatMessages.length;
|
|
1139
|
+
|
|
1140
|
+
pluginApi.sendGenericDataForLearningAnalyticsDashboard({
|
|
1141
|
+
cardTitle: 'Chat Activity',
|
|
1142
|
+
columnTitle: 'Messages Sent',
|
|
1143
|
+
value: messageCount.toString(),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}, [chatMessages]);
|
|
1147
|
+
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
**Use cases for Learning Analytics:**
|
|
1151
|
+
- Track user participation and engagement
|
|
1152
|
+
- Monitor plugin-specific metrics (e.g., poll responses, quiz answers)
|
|
1153
|
+
- Measure feature usage and adoption
|
|
1154
|
+
- Collect custom learning indicators
|
|
1155
|
+
- Generate post-meeting reports with plugin data
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
See example of use ahead:
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
const targetUserId = 'abcd-efg';
|
|
1162
|
+
pluginApi.learningAnalyticsDashboard.upsertUserData(
|
|
1163
|
+
{
|
|
1164
|
+
cardTitle: 'Example Title',
|
|
1165
|
+
columnTitle: 'link sent by user',
|
|
1166
|
+
value: '[link](https://my-website.com/abc.png)'
|
|
1167
|
+
},
|
|
1168
|
+
targetUserId,
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
pluginApi.learningAnalyticsDashboard.deleteUserData(
|
|
1172
|
+
{
|
|
1173
|
+
cardTitle: 'Example Title',
|
|
1174
|
+
columnTitle: 'link sent by user',
|
|
1175
|
+
},
|
|
1176
|
+
targetUserId,
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
pluginApi.learningAnalyticsDashboard.clearAllUsersData(columnTitle);
|
|
1180
|
+
|
|
1181
|
+
pluginApi.learningAnalyticsDashboard.clearAllUsersData(); // Or without the Column Title
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
Note 1: the `value` field (in the upsert function's argument) supports markdown, so feel free to use it as you wish.
|
|
1185
|
+
|
|
1186
|
+
Note 2: pluginApi.sendGenericDataForLearningAnalyticsDashboard is now being deprecated, but has the same data structure as upsert (without the possibility to send entry on behalf of another user)
|
|
497
1187
|
|
|
498
1188
|
### External data resources
|
|
499
1189
|
|
|
@@ -549,12 +1239,167 @@ pluginApi.getRemoteData('allUsers').then((response: Response) => {
|
|
|
549
1239
|
});
|
|
550
1240
|
```
|
|
551
1241
|
|
|
552
|
-
###
|
|
1242
|
+
### Fetch ui data on demand
|
|
1243
|
+
|
|
1244
|
+
- `getUiData` async function: This will return certain data from the UI depending on the parameter the developer uses. Unlike the `useUiData` this function does not return real-time information as it changes. See the currently supported:
|
|
1245
|
+
- PresentationWhiteboardUiDataNames.CURRENT_PAGE_SNAPSHOT;
|
|
1246
|
+
|
|
1247
|
+
**Example usage:**
|
|
1248
|
+
|
|
1249
|
+
```typescript
|
|
1250
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1251
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1252
|
+
|
|
1253
|
+
useEffect(() => {
|
|
1254
|
+
pluginApi.setActionButtonDropdownItems([
|
|
1255
|
+
new ActionButtonDropdownOption({
|
|
1256
|
+
label: 'Capture whiteboard snapshot',
|
|
1257
|
+
icon: 'copy',
|
|
1258
|
+
onClick: async () => {
|
|
1259
|
+
// Fetch the current whiteboard page as a PNG snapshot
|
|
1260
|
+
const snapshotData = await pluginApi.getUiData(
|
|
1261
|
+
PresentationWhiteboardUiDataNames.CURRENT_PAGE_SNAPSHOT
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
if (snapshotData?.pngBase64) {
|
|
1265
|
+
console.log('Whiteboard snapshot (base64 PNG):', snapshotData.pngBase64);
|
|
1266
|
+
|
|
1267
|
+
// Example: Download the snapshot
|
|
1268
|
+
const link = document.createElement('a');
|
|
1269
|
+
link.href = `data:image/png;base64,${snapshotData.pngBase64}`;
|
|
1270
|
+
link.download = `whiteboard-snapshot-${Date.now()}.png`;
|
|
1271
|
+
link.click();
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
}),
|
|
1275
|
+
]);
|
|
1276
|
+
}, []);
|
|
1277
|
+
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
### Customize manifest.json
|
|
1281
|
+
|
|
1282
|
+
The following sections explain how you can dynamically customize your manifest.json for different runs.
|
|
1283
|
+
|
|
1284
|
+
#### Meta_ parameters
|
|
553
1285
|
|
|
554
1286
|
This is not part of the API, but it's a way of passing information to the manifest. Any value can be passed like this, one just needs to put something like `${meta_nameOfParameter}` in a specific config of the manifest, and in the `/create` call, set this meta-parameter to whatever is preferred, like `meta_nameOfParameter="Sample message"`
|
|
555
1287
|
|
|
556
1288
|
This feature is mainly used for security purposes, see [external data section](#external-data-resources). But can be used for customization reasons as well.
|
|
557
1289
|
|
|
1290
|
+
**Example:**
|
|
1291
|
+
|
|
1292
|
+
```json
|
|
1293
|
+
{
|
|
1294
|
+
"requiredSdkVersion": "~0.0.59",
|
|
1295
|
+
"name": "MyPlugin",
|
|
1296
|
+
"javascriptEntrypointUrl": "MyPlugin.js",
|
|
1297
|
+
"remoteDataSources": [
|
|
1298
|
+
{
|
|
1299
|
+
"name": "userData",
|
|
1300
|
+
"url": "${meta_userDataEndpoint}",
|
|
1301
|
+
"fetchMode": "onMeetingCreate",
|
|
1302
|
+
"permissions": ["moderator", "viewer"]
|
|
1303
|
+
}
|
|
1304
|
+
]
|
|
1305
|
+
}
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
Then in the `/create` call:
|
|
1309
|
+
|
|
1310
|
+
```
|
|
1311
|
+
meta_userDataEndpoint=https://my-api.com/users
|
|
1312
|
+
pluginManifests=[{"url": "https://my-cdn.com/my-plugin/manifest.json"}]
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
#### Plugin_ parameters
|
|
1316
|
+
|
|
1317
|
+
`plugin_` parameters work similarly to `meta_` parameters, allowing data to be passed dynamically to the manifest. While they can serve the same purposes — like security or customization — they are specifically scoped to individual plugins.
|
|
1318
|
+
|
|
1319
|
+
**Format:**
|
|
1320
|
+
|
|
1321
|
+
```
|
|
1322
|
+
plugin_<pluginName>_<parameter-name>
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
- `<pluginName>` — The name of the plugin as defined in `manifest.json`.
|
|
1326
|
+
- `<parameter-name>` — The parameter's name. It may include letters (uppercase or lowercase), numbers and hyphens (`-`).
|
|
1327
|
+
|
|
1328
|
+
This naming convention ensures that each plugin has its own namespace for parameters. Other plugins cannot access values outside their own namespace. For example:
|
|
1329
|
+
|
|
1330
|
+
```
|
|
1331
|
+
plugin_MyPlugin_api-endpoint=https://api.example.com
|
|
1332
|
+
plugin_MyPlugin_theme-color=blue
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
This isolates the parameter to `MyPlugin` and avoids conflicts with other plugins.
|
|
1336
|
+
|
|
1337
|
+
**Example manifest.json:**
|
|
1338
|
+
|
|
1339
|
+
```json
|
|
1340
|
+
{
|
|
1341
|
+
"requiredSdkVersion": "~0.0.59",
|
|
1342
|
+
"name": "MyPlugin",
|
|
1343
|
+
"javascriptEntrypointUrl": "MyPlugin.js",
|
|
1344
|
+
"dataChannels": [
|
|
1345
|
+
{
|
|
1346
|
+
"name": "${plugin_MyPlugin_channel-name:defaultChannel}",
|
|
1347
|
+
"pushPermission": ["moderator", "presenter"],
|
|
1348
|
+
"replaceOrDeletePermission": ["moderator", "creator"]
|
|
1349
|
+
}
|
|
1350
|
+
],
|
|
1351
|
+
"remoteDataSources": [
|
|
1352
|
+
{
|
|
1353
|
+
"name": "pluginData",
|
|
1354
|
+
"url": "${plugin_MyPlugin_api-endpoint}",
|
|
1355
|
+
"fetchMode": "onDemand",
|
|
1356
|
+
"permissions": ["moderator"]
|
|
1357
|
+
}
|
|
1358
|
+
]
|
|
1359
|
+
}
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
Then in the `/create` call:
|
|
1363
|
+
|
|
1364
|
+
```
|
|
1365
|
+
plugin_MyPlugin_channel-name=customChannelName
|
|
1366
|
+
plugin_MyPlugin_api-endpoint=https://my-api.com/plugin-data
|
|
1367
|
+
pluginManifests=[{"url": "https://my-cdn.com/my-plugin/manifest.json"}]
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
#### Default value (fallback) for missing placeholder's parameters
|
|
1371
|
+
|
|
1372
|
+
If a plugin expects a placeholder (via `meta_` or `plugin_`) but doesn't receive a value, the plugin will fail to load. To prevent this, both types of placeholders support default values. This allows the system administrator to define fallback values, ensuring the plugin loads correctly.
|
|
1373
|
+
|
|
1374
|
+
**Example with a default value:**
|
|
1375
|
+
|
|
1376
|
+
```json
|
|
1377
|
+
{
|
|
1378
|
+
"requiredSdkVersion": "~0.0.59",
|
|
1379
|
+
"name": "MyPlugin",
|
|
1380
|
+
"javascriptEntrypointUrl": "MyPlugin.js",
|
|
1381
|
+
"dataChannels": [
|
|
1382
|
+
{
|
|
1383
|
+
"name": "${plugin_MyPlugin_data-channel-name:storeState}",
|
|
1384
|
+
"pushPermission": ["moderator", "presenter"],
|
|
1385
|
+
"replaceOrDeletePermission": ["moderator", "creator"]
|
|
1386
|
+
}
|
|
1387
|
+
],
|
|
1388
|
+
"remoteDataSources": [
|
|
1389
|
+
{
|
|
1390
|
+
"name": "apiData",
|
|
1391
|
+
"url": "${meta_apiEndpoint:https://default-api.com/data}",
|
|
1392
|
+
"fetchMode": "onMeetingCreate",
|
|
1393
|
+
"permissions": ["moderator"]
|
|
1394
|
+
}
|
|
1395
|
+
]
|
|
1396
|
+
}
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
In this example:
|
|
1400
|
+
- If `plugin_MyPlugin_data-channel-name` is not provided, it will fall back to `"storeState"`
|
|
1401
|
+
- If `meta_apiEndpoint` is not provided, it will fall back to `"https://default-api.com/data"`
|
|
1402
|
+
|
|
558
1403
|
### Event persistence
|
|
559
1404
|
|
|
560
1405
|
This feature will allow the developer to save an information (an event) in the `event.xml` file of the meeting, if it's being recorded.
|
|
@@ -608,6 +1453,185 @@ Where `<meeting-id>` is the id of the the meeting you just recorded. Then, among
|
|
|
608
1453
|
</event>
|
|
609
1454
|
```
|
|
610
1455
|
|
|
1456
|
+
## Common Patterns and Best Practices
|
|
1457
|
+
|
|
1458
|
+
This section covers common patterns and best practices for plugin development.
|
|
1459
|
+
|
|
1460
|
+
### Plugin Initialization Pattern
|
|
1461
|
+
|
|
1462
|
+
Always initialize your plugin in this order:
|
|
1463
|
+
|
|
1464
|
+
```typescript
|
|
1465
|
+
import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk';
|
|
1466
|
+
import { useEffect } from 'react';
|
|
1467
|
+
|
|
1468
|
+
function MyPlugin({ pluginUuid }: MyPluginProps) {
|
|
1469
|
+
// 1. Initialize the SDK (must be first)
|
|
1470
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1471
|
+
|
|
1472
|
+
// 2. Get the plugin API instance
|
|
1473
|
+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1474
|
+
|
|
1475
|
+
// 3. Use hooks to fetch data
|
|
1476
|
+
const { data: currentUser } = pluginApi.useCurrentUser();
|
|
1477
|
+
|
|
1478
|
+
// 4. Set up UI extensions in useEffect
|
|
1479
|
+
useEffect(() => {
|
|
1480
|
+
// Register UI items here
|
|
1481
|
+
pluginApi.setActionButtonDropdownItems([...]);
|
|
1482
|
+
}, []); // Empty dependency array for one-time setup
|
|
1483
|
+
|
|
1484
|
+
// 5. Return null (plugins typically don't render directly)
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
### Error Handling Pattern
|
|
1490
|
+
|
|
1491
|
+
```typescript
|
|
1492
|
+
function MyPlugin({ pluginUuid }: MyPluginProps) {
|
|
1493
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1494
|
+
const pluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1495
|
+
|
|
1496
|
+
const { data, loading, error } = pluginApi.useCurrentUser();
|
|
1497
|
+
|
|
1498
|
+
useEffect(() => {
|
|
1499
|
+
if (error) {
|
|
1500
|
+
pluginLogger.error('Error fetching user data:', error);
|
|
1501
|
+
// Show user-friendly notification
|
|
1502
|
+
pluginApi.uiCommands.notification.send({
|
|
1503
|
+
type: 'error',
|
|
1504
|
+
message: 'Failed to load plugin. Please refresh the page.',
|
|
1505
|
+
icon: 'warning',
|
|
1506
|
+
});
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (loading) {
|
|
1511
|
+
pluginLogger.info('Loading user data...');
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (data) {
|
|
1516
|
+
// Proceed with plugin logic
|
|
1517
|
+
pluginLogger.info('User data loaded successfully');
|
|
1518
|
+
}
|
|
1519
|
+
}, [data, loading, error]);
|
|
1520
|
+
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
### State Management Pattern
|
|
1526
|
+
|
|
1527
|
+
```typescript
|
|
1528
|
+
function MyPlugin({ pluginUuid }: MyPluginProps) {
|
|
1529
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1530
|
+
const pluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1531
|
+
const [count, setCount] = useState(0);
|
|
1532
|
+
const [isActive, setIsActive] = useState(false);
|
|
1533
|
+
|
|
1534
|
+
useEffect(() => {
|
|
1535
|
+
pluginApi.setActionButtonDropdownItems([
|
|
1536
|
+
new ActionButtonDropdownOption({
|
|
1537
|
+
label: `Counter: ${count}`,
|
|
1538
|
+
icon: 'copy',
|
|
1539
|
+
onClick: () => {
|
|
1540
|
+
setCount(prevCount => prevCount + 1);
|
|
1541
|
+
setIsActive(true);
|
|
1542
|
+
},
|
|
1543
|
+
}),
|
|
1544
|
+
]);
|
|
1545
|
+
}, [count]); // Re-render when count changes
|
|
1546
|
+
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
### Data Channel Communication Pattern
|
|
1552
|
+
|
|
1553
|
+
```typescript
|
|
1554
|
+
interface MessageType {
|
|
1555
|
+
userId: string;
|
|
1556
|
+
message: string;
|
|
1557
|
+
timestamp: number;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function MyPlugin({ pluginUuid }: MyPluginProps) {
|
|
1561
|
+
BbbPluginSdk.initialize(pluginUuid);
|
|
1562
|
+
const pluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
|
|
1563
|
+
|
|
1564
|
+
const {
|
|
1565
|
+
data: messages,
|
|
1566
|
+
pushEntry,
|
|
1567
|
+
deleteEntry,
|
|
1568
|
+
} = pluginApi.useDataChannel<MessageType>(
|
|
1569
|
+
'my-channel',
|
|
1570
|
+
DataChannelTypes.ALL_ITEMS,
|
|
1571
|
+
'default'
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
const sendMessage = (text: string) => {
|
|
1575
|
+
if (pushEntry) {
|
|
1576
|
+
pushEntry({
|
|
1577
|
+
userId: 'current-user-id',
|
|
1578
|
+
message: text,
|
|
1579
|
+
timestamp: Date.now(),
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
const clearMessages = () => {
|
|
1585
|
+
if (deleteEntry) {
|
|
1586
|
+
deleteEntry(RESET_DATA_CHANNEL);
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
useEffect(() => {
|
|
1591
|
+
pluginApi.setActionButtonDropdownItems([
|
|
1592
|
+
new ActionButtonDropdownOption({
|
|
1593
|
+
label: 'Send Message',
|
|
1594
|
+
icon: 'chat',
|
|
1595
|
+
onClick: () => sendMessage('Hello!'),
|
|
1596
|
+
}),
|
|
1597
|
+
new ActionButtonDropdownOption({
|
|
1598
|
+
label: 'Clear Messages',
|
|
1599
|
+
icon: 'delete',
|
|
1600
|
+
onClick: clearMessages,
|
|
1601
|
+
}),
|
|
1602
|
+
]);
|
|
1603
|
+
}, []);
|
|
1604
|
+
|
|
1605
|
+
useEffect(() => {
|
|
1606
|
+
if (messages?.data) {
|
|
1607
|
+
pluginLogger.info('Messages:', messages.data);
|
|
1608
|
+
}
|
|
1609
|
+
}, [messages]);
|
|
1610
|
+
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
### Best Practices
|
|
1616
|
+
|
|
1617
|
+
1. **Always initialize the SDK first** before using any plugin API
|
|
1618
|
+
2. **Use TypeScript** for type safety and better development experience
|
|
1619
|
+
3. **Handle loading and error states** gracefully
|
|
1620
|
+
4. **Clean up resources** (intervals, event listeners) in useEffect cleanup
|
|
1621
|
+
5. **Use pluginLogger** instead of console.log for better debugging
|
|
1622
|
+
6. **Test your plugin** with different user roles (moderator, presenter, viewer)
|
|
1623
|
+
7. **Use the samples** as reference - they demonstrate best practices
|
|
1624
|
+
8. **Document your plugin** - include a README with usage instructions
|
|
1625
|
+
|
|
1626
|
+
### Performance Tips
|
|
1627
|
+
|
|
1628
|
+
- **Minimize re-renders**: Use proper dependency arrays in useEffect
|
|
1629
|
+
- **Avoid excessive DOM manipulation**: Use the provided hooks when possible
|
|
1630
|
+
- **Debounce frequent operations**: Use debouncing for frequent UI updates
|
|
1631
|
+
- **Clean up subscriptions**: Always return cleanup functions from useEffect
|
|
1632
|
+
- **Use memoization**: Use React.useMemo and React.useCallback for expensive operations
|
|
1633
|
+
|
|
1634
|
+
|
|
611
1635
|
### Frequently Asked Questions (FAQ)
|
|
612
1636
|
|
|
613
1637
|
**How do I remove a certain extensible area that I don't want anymore?**
|