bigbluebutton-html-plugin-sdk 0.1.13 → 0.1.14

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