@webex/plugin-meetings 3.8.1-next.1 → 3.8.1-next.10

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 (44) hide show
  1. package/README.md +26 -13
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/constants.js +6 -1
  5. package/dist/constants.js.map +1 -1
  6. package/dist/controls-options-manager/enums.js +1 -0
  7. package/dist/controls-options-manager/enums.js.map +1 -1
  8. package/dist/controls-options-manager/types.js.map +1 -1
  9. package/dist/controls-options-manager/util.js +26 -0
  10. package/dist/controls-options-manager/util.js.map +1 -1
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/controlsUtils.js +8 -2
  14. package/dist/locus-info/controlsUtils.js.map +1 -1
  15. package/dist/locus-info/index.js +9 -0
  16. package/dist/locus-info/index.js.map +1 -1
  17. package/dist/meeting/in-meeting-actions.js +5 -1
  18. package/dist/meeting/in-meeting-actions.js.map +1 -1
  19. package/dist/meeting/index.js +39 -32
  20. package/dist/meeting/index.js.map +1 -1
  21. package/dist/reachability/index.js +5 -10
  22. package/dist/reachability/index.js.map +1 -1
  23. package/dist/types/constants.d.ts +4 -0
  24. package/dist/types/controls-options-manager/enums.d.ts +2 -1
  25. package/dist/types/controls-options-manager/types.d.ts +4 -1
  26. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  27. package/dist/types/reachability/index.d.ts +2 -2
  28. package/dist/webinar/index.js +1 -1
  29. package/package.json +23 -23
  30. package/src/constants.ts +6 -0
  31. package/src/controls-options-manager/enums.ts +1 -0
  32. package/src/controls-options-manager/types.ts +6 -1
  33. package/src/controls-options-manager/util.ts +31 -0
  34. package/src/locus-info/controlsUtils.ts +9 -0
  35. package/src/locus-info/index.ts +9 -0
  36. package/src/meeting/in-meeting-actions.ts +8 -0
  37. package/src/meeting/index.ts +33 -22
  38. package/src/reachability/index.ts +5 -13
  39. package/test/unit/spec/controls-options-manager/util.js +58 -0
  40. package/test/unit/spec/locus-info/controlsUtils.js +16 -0
  41. package/test/unit/spec/locus-info/index.js +14 -0
  42. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  43. package/test/unit/spec/meeting/index.js +76 -26
  44. package/test/unit/spec/reachability/index.ts +2 -6
@@ -14,6 +14,7 @@ enum Control {
14
14
  viewTheParticipantList = 'viewTheParticipantList',
15
15
  annotation = 'annotation',
16
16
  rdc = 'rdc',
17
+ pollingQA = 'pollingQA',
17
18
  }
18
19
 
19
20
  export {Control, Setting};
@@ -48,6 +48,10 @@ export interface RemoteDesktopControlProperties {
48
48
  enabled?: boolean;
49
49
  }
50
50
 
51
+ export interface PollingQAProperties {
52
+ enabled?: boolean;
53
+ }
54
+
51
55
  export type Properties =
52
56
  | AudioProperties
53
57
  | RaiseHandProperties
@@ -56,7 +60,8 @@ export type Properties =
56
60
  | VideoProperties
57
61
  | ViewTheParticipantListProperties
58
62
  | AnnotationProperties
59
- | RemoteDesktopControlProperties;
63
+ | RemoteDesktopControlProperties
64
+ | PollingQAProperties;
60
65
 
61
66
  export interface ControlConfig<Props = Properties> {
62
67
  /**
@@ -9,6 +9,7 @@ import {
9
9
  VideoProperties,
10
10
  type RemoteDesktopControlProperties,
11
11
  type AnnotationProperties,
12
+ type PollingQAProperties,
12
13
  } from './types';
13
14
 
14
15
  /**
@@ -304,6 +305,29 @@ class Utils {
304
305
  return Utils.hasHints({requiredHints, displayHints});
305
306
  }
306
307
 
308
+ /**
309
+ * Validate if a pollingQA-scoped control is allowed to be sent to the service.
310
+ *
311
+ * @param {ControlConfig<PollingQAProperties>} control - Polling QA config to validate
312
+ * @param {Array<string>} displayHints - All available hints
313
+ * @returns {boolean} - True if all of the actions are allowed.
314
+ */
315
+ public static canUpdatePollingQA(
316
+ control: ControlConfig<PollingQAProperties>,
317
+ displayHints: Array<string>
318
+ ): boolean {
319
+ const requiredHints = [];
320
+
321
+ if (control.properties.enabled === true) {
322
+ requiredHints.push(DISPLAY_HINTS.ENABLE_ATTENDEE_START_POLLING_QA);
323
+ }
324
+ if (control.properties.enabled === false) {
325
+ requiredHints.push(DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA);
326
+ }
327
+
328
+ return Utils.hasHints({requiredHints, displayHints});
329
+ }
330
+
307
331
  /**
308
332
  * Validate that a control can be sent to the service based on the provided
309
333
  * display hints.
@@ -363,6 +387,13 @@ class Utils {
363
387
  );
364
388
  break;
365
389
 
390
+ case Control.pollingQA:
391
+ determinant = Utils.canUpdatePollingQA(
392
+ control as ControlConfig<PollingQAProperties>,
393
+ displayHints
394
+ );
395
+ break;
396
+
366
397
  default:
367
398
  determinant = false;
368
399
  }
@@ -124,6 +124,12 @@ ControlsUtils.parse = (controls: any) => {
124
124
  };
125
125
  }
126
126
 
127
+ if (controls?.pollingQAControl) {
128
+ parsedControls.pollingQAControl = {
129
+ enabled: controls.pollingQAControl.enabled,
130
+ };
131
+ }
132
+
127
133
  return parsedControls;
128
134
  };
129
135
 
@@ -235,6 +241,9 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => {
235
241
 
236
242
  hasRemoteDesktopControlChanged:
237
243
  current?.rdcControl?.enabled !== previous?.rdcControl?.enabled,
244
+
245
+ hasPollingQAControlChanged:
246
+ current?.pollingQAControl?.enabled !== previous?.pollingQAControl?.enabled,
238
247
  },
239
248
  };
240
249
  };
@@ -854,6 +854,7 @@ export default class LocusInfo extends EventsScope {
854
854
  hasStageViewChanged,
855
855
  hasAnnotationControlChanged,
856
856
  hasRemoteDesktopControlChanged,
857
+ hasPollingQAControlChanged,
857
858
  },
858
859
  current,
859
860
  } = ControlsUtils.getControls(this.controls, controls);
@@ -1120,6 +1121,14 @@ export default class LocusInfo extends EventsScope {
1120
1121
  );
1121
1122
  }
1122
1123
 
1124
+ if (hasPollingQAControlChanged) {
1125
+ this.emitScoped(
1126
+ {file: 'locus-info', function: 'updateControls'},
1127
+ LOCUSINFO.EVENTS.CONTROLS_POLLING_QA_CHANGED,
1128
+ {state: current.pollingQAControl}
1129
+ );
1130
+ }
1131
+
1123
1132
  this.controls = controls;
1124
1133
  }
1125
1134
  }
@@ -107,6 +107,8 @@ interface IInMeetingActions {
107
107
  canEnableRemoteDesktopControl?: boolean;
108
108
  canDisableRemoteDesktopControl?: boolean;
109
109
  canMoveToLobby?: boolean;
110
+ canEnablePollingQA?: boolean;
111
+ canDisablePollingQA?: boolean;
110
112
  }
111
113
 
112
114
  /**
@@ -309,6 +311,10 @@ export default class InMeetingActions implements IInMeetingActions {
309
311
 
310
312
  canMoveToLobby = null;
311
313
 
314
+ canEnablePollingQA = null;
315
+
316
+ canDisablePollingQA = null;
317
+
312
318
  /**
313
319
  * Returns all meeting action options
314
320
  * @returns {Object}
@@ -411,6 +417,8 @@ export default class InMeetingActions implements IInMeetingActions {
411
417
  canEnableRemoteDesktopControl: this.canEnableRemoteDesktopControl,
412
418
  canDisableRemoteDesktopControl: this.canDisableRemoteDesktopControl,
413
419
  canMoveToLobby: this.canMoveToLobby,
420
+ canEnablePollingQA: this.canEnablePollingQA,
421
+ canDisablePollingQA: this.canDisablePollingQA,
414
422
  });
415
423
 
416
424
  /**
@@ -263,8 +263,9 @@ type FetchMeetingInfoParams = {
263
263
  };
264
264
 
265
265
  type MediaReachabilityMetrics = ReachabilityMetrics & {
266
- isSubnetReachable: boolean;
267
- selectedCluster: string | null;
266
+ subnet_reachable: boolean;
267
+ selected_cluster: string | null;
268
+ selected_subnet: string | null;
268
269
  };
269
270
 
270
271
  /**
@@ -2757,7 +2758,9 @@ export default class Meeting extends StatelessWebexPlugin {
2757
2758
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
2758
2759
  ({spokenLanguage}) => {
2759
2760
  if (spokenLanguage) {
2760
- this.transcription.languageOptions.currentSpokenLanguage = spokenLanguage;
2761
+ if (this.transcription?.languageOptions) {
2762
+ this.transcription.languageOptions.currentSpokenLanguage = spokenLanguage;
2763
+ }
2761
2764
  // @ts-ignore
2762
2765
  this.webex.internal.voicea.onSpokenLanguageUpdate(spokenLanguage);
2763
2766
 
@@ -2942,6 +2945,15 @@ export default class Meeting extends StatelessWebexPlugin {
2942
2945
  {state}
2943
2946
  );
2944
2947
  });
2948
+
2949
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_POLLING_QA_CHANGED, ({state}) => {
2950
+ Trigger.trigger(
2951
+ this,
2952
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2953
+ EVENT_TRIGGERS.MEETING_CONTROLS_POLLING_QA_UPDATED,
2954
+ {state}
2955
+ );
2956
+ });
2945
2957
  }
2946
2958
 
2947
2959
  /**
@@ -4383,6 +4395,14 @@ export default class Meeting extends StatelessWebexPlugin {
4383
4395
  requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
4384
4396
  displayHints: this.userDisplayHints,
4385
4397
  }),
4398
+ canEnablePollingQA: ControlsOptionsUtil.hasHints({
4399
+ requiredHints: [DISPLAY_HINTS.ENABLE_ATTENDEE_START_POLLING_QA],
4400
+ displayHints: this.userDisplayHints,
4401
+ }),
4402
+ canDisablePollingQA: ControlsOptionsUtil.hasHints({
4403
+ requiredHints: [DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA],
4404
+ displayHints: this.userDisplayHints,
4405
+ }),
4386
4406
  }) || changed;
4387
4407
  }
4388
4408
  if (changed) {
@@ -4898,11 +4918,6 @@ export default class Meeting extends StatelessWebexPlugin {
4898
4918
 
4899
4919
  // Only send restore event when it was disconnected before and for connected later
4900
4920
  if (!this.hasWebsocketConnected) {
4901
- // @ts-ignore
4902
- this.webex.internal.newMetrics.submitClientEvent({
4903
- name: 'client.mercury.connection.restored',
4904
- options: {meetingId: this.id},
4905
- });
4906
4921
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_RESTORED, {
4907
4922
  correlation_id: this.correlationId,
4908
4923
  });
@@ -4913,11 +4928,6 @@ export default class Meeting extends StatelessWebexPlugin {
4913
4928
  // @ts-ignore
4914
4929
  this.webex.internal.mercury.on(OFFLINE, () => {
4915
4930
  LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
4916
- // @ts-ignore
4917
- this.webex.internal.newMetrics.submitClientEvent({
4918
- name: 'client.mercury.connection.lost',
4919
- options: {meetingId: this.id},
4920
- });
4921
4931
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
4922
4932
  correlation_id: this.correlationId,
4923
4933
  });
@@ -9714,21 +9724,22 @@ export default class Meeting extends StatelessWebexPlugin {
9714
9724
  return total;
9715
9725
  }, 0);
9716
9726
 
9727
+ const selectedSubnetFirstOctet = this.mediaServerIp?.split('.')[0];
9728
+
9717
9729
  let isSubnetReachable = null;
9718
- if (totalSuccessCases > 0) {
9719
- // @ts-ignore
9720
- isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(this.mediaServerIp);
9730
+ if (totalSuccessCases > 0 && selectedSubnetFirstOctet) {
9731
+ isSubnetReachable =
9732
+ // @ts-ignore
9733
+ this.webex.meetings.reachability.isSubnetReachable(selectedSubnetFirstOctet);
9721
9734
  }
9722
9735
 
9723
- let selectedCluster = null;
9724
- if (this.mediaConnections && this.mediaConnections.length > 0) {
9725
- selectedCluster = this.mediaConnections[0].mediaAgentCluster;
9726
- }
9736
+ const selectedCluster = this.mediaConnections?.[0]?.mediaAgentCluster ?? null;
9727
9737
 
9728
9738
  return {
9729
9739
  ...reachabilityMetrics,
9730
- isSubnetReachable,
9731
- selectedCluster,
9740
+ subnet_reachable: isSubnetReachable,
9741
+ selected_cluster: selectedCluster,
9742
+ selected_subnet: selectedSubnetFirstOctet ? `${selectedSubnetFirstOctet}.X.X.X` : null,
9732
9743
  };
9733
9744
  }
9734
9745
  }
@@ -140,22 +140,14 @@ export default class Reachability extends EventsScope {
140
140
 
141
141
  /**
142
142
  * Checks if the given subnet is reachable
143
- * @param {string} mediaServerIp - media server ip
143
+ * @param {string} selectedSubnetFirstOctet - selected subnet first octet, e.g. "10" for "10.X.X.X"
144
144
  * @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
145
145
  * @public
146
146
  * @memberof Reachability
147
147
  */
148
- public isSubnetReachable(mediaServerIp?: string): boolean | null {
149
- if (!mediaServerIp) {
150
- LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);
151
-
152
- return null;
153
- }
154
-
155
- const subnetFirstOctet = mediaServerIp.split('.')[0];
156
-
148
+ public isSubnetReachable(selectedSubnetFirstOctet: string): boolean | null {
157
149
  LoggerProxy.logger.info(
158
- `Reachability:index#isSubnetReachable --> Looking for subnet: ${subnetFirstOctet}.X.X.X`
150
+ `Reachability:index#isSubnetReachable --> Looking for subnet: ${selectedSubnetFirstOctet}.X.X.X`
159
151
  );
160
152
 
161
153
  const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
@@ -167,7 +159,7 @@ export default class Reachability extends EventsScope {
167
159
  const subnet = reachedSubnetsArray[i];
168
160
  const reachedSubnetFirstOctet = subnet.split('.')[0];
169
161
 
170
- if (subnetFirstOctet === reachedSubnetFirstOctet) {
162
+ if (selectedSubnetFirstOctet === reachedSubnetFirstOctet) {
171
163
  acc.add(cluster.name);
172
164
  }
173
165
 
@@ -186,7 +178,7 @@ export default class Reachability extends EventsScope {
186
178
  );
187
179
 
188
180
  LoggerProxy.logger.info(
189
- `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${subnetFirstOctet}.X.X.X`
181
+ `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${selectedSubnetFirstOctet}.X.X.X`
190
182
  );
191
183
 
192
184
  return matchingReachedClusters.size > 0;
@@ -473,6 +473,40 @@ describe('plugin-meetings', () => {
473
473
  assert.equal(results, expected);
474
474
  });
475
475
  });
476
+
477
+ describe('canUpdatePollingQA()', () => {
478
+ beforeEach(() => {
479
+ sinon.stub(ControlsOptionsUtil, 'hasHints').returns(true);
480
+ });
481
+
482
+ it('should call hasHints() with proper hints when `enabled` is true', () => {
483
+ ControlsOptionsUtil.canUpdatePollingQA({properties: {enabled: true}}, []);
484
+
485
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
486
+ requiredHints: [DISPLAY_HINTS.ENABLE_ATTENDEE_START_POLLING_QA],
487
+ displayHints: [],
488
+ });
489
+ });
490
+
491
+ it('should call hasHints() with proper hints when `enabled` is false', () => {
492
+ ControlsOptionsUtil.canUpdatePollingQA({properties: {enabled: false}}, []);
493
+
494
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
495
+ requiredHints: [DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA],
496
+ displayHints: [],
497
+ });
498
+ });
499
+
500
+ it('should return the resolution of hasHints()', () => {
501
+ const expected = 'example-return-value';
502
+ ControlsOptionsUtil.hasHints.returns(expected);
503
+
504
+ const results = ControlsOptionsUtil.canUpdatePollingQA({properties: {}}, []);
505
+
506
+ assert.calledOnce(ControlsOptionsUtil.hasHints);
507
+ assert.equal(results, expected);
508
+ });
509
+ });
476
510
 
477
511
  describe('canUpdate()', () => {
478
512
  const displayHints = [];
@@ -486,6 +520,7 @@ describe('plugin-meetings', () => {
486
520
  ControlsOptionsUtil.canUpdateViewTheParticipantsList = sinon.stub().returns(true);
487
521
  ControlsOptionsUtil.canUpdateAnnotation = sinon.stub().returns(true);
488
522
  ControlsOptionsUtil.canUpdateRemoteDesktopControl = sinon.stub().returns(true);
523
+ ControlsOptionsUtil.canUpdatePollingQA = sinon.stub().returns(true);
489
524
  });
490
525
 
491
526
  it('should only call canUpdateAudio() if the scope is audio', () => {
@@ -621,6 +656,28 @@ describe('plugin-meetings', () => {
621
656
  control,
622
657
  displayHints
623
658
  );
659
+ assert.callCount(ControlsOptionsUtil.canUpdatePollingQA, 0);
660
+ assert.isTrue(results);
661
+ });
662
+
663
+ it('should only call canUpdatePollingQA() if the scope is pollingQA', () => {
664
+ const control = {scope: 'pollingQA'};
665
+
666
+ const results = ControlsOptionsUtil.canUpdate(control, displayHints);
667
+
668
+ assert.callCount(ControlsOptionsUtil.canUpdateAudio, 0);
669
+ assert.callCount(ControlsOptionsUtil.canUpdateRaiseHand, 0);
670
+ assert.callCount(ControlsOptionsUtil.canUpdateReactions, 0);
671
+ assert.callCount(ControlsOptionsUtil.canUpdateShareControl, 0);
672
+ assert.callCount(ControlsOptionsUtil.canUpdateVideo, 0);
673
+ assert.callCount(ControlsOptionsUtil.canUpdateViewTheParticipantsList, 0);
674
+ assert.callCount(ControlsOptionsUtil.canUpdateAnnotation, 0);
675
+ assert.callCount(ControlsOptionsUtil.canUpdateRemoteDesktopControl, 0);
676
+ assert.calledWith(
677
+ ControlsOptionsUtil.canUpdatePollingQA,
678
+ control,
679
+ displayHints
680
+ );
624
681
  assert.isTrue(results);
625
682
  });
626
683
 
@@ -637,6 +694,7 @@ describe('plugin-meetings', () => {
637
694
  assert.callCount(ControlsOptionsUtil.canUpdateViewTheParticipantsList, 0);
638
695
  assert.callCount(ControlsOptionsUtil.canUpdateAnnotation, 0);
639
696
  assert.callCount(ControlsOptionsUtil.canUpdateRemoteDesktopControl, 0);
697
+ assert.callCount(ControlsOptionsUtil.canUpdatePollingQA, 0);
640
698
  assert.isFalse(results);
641
699
  });
642
700
  });
@@ -164,6 +164,14 @@ describe('plugin-meetings', () => {
164
164
 
165
165
  assert.equal(parsedControls.rdcControl.enabled, newControls.rdcControl.enabled);
166
166
  });
167
+
168
+ it('should parse the pollingQAControl control', () => {
169
+ const newControls = {pollingQAControl: {enabled: true}};
170
+
171
+ const parsedControls = ControlsUtils.parse(newControls);
172
+
173
+ assert.equal(parsedControls.pollingQAControl.enabled, newControls.pollingQAControl.enabled);
174
+ });
167
175
 
168
176
  describe('videoEnabled', () => {
169
177
  it('returns expected', () => {
@@ -411,6 +419,14 @@ describe('plugin-meetings', () => {
411
419
  assert.equal(updates.hasRemoteDesktopControlChanged, true);
412
420
  });
413
421
 
422
+ it('returns hasPollingQAControlChanged = true when changed', () => {
423
+ const newControls = {pollingQAControl: {enabled: true}};
424
+
425
+ const {updates} = ControlsUtils.getControls(defaultControls, newControls);
426
+
427
+ assert.equal(updates.hasPollingQAControlChanged, true);
428
+ });
429
+
414
430
  it('returns false when previous spoken language is undefined and current is a invalid value', () => {
415
431
  const previous = { transcribe: undefined };
416
432
  const current = { transcribe: { spokenLanguage: null } };
@@ -305,6 +305,20 @@ describe('plugin-meetings', () => {
305
305
  {state: newControls.rdcControl}
306
306
  );
307
307
  });
308
+
309
+ it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
310
+ locusInfo.controls = {};
311
+ locusInfo.emitScoped = sinon.stub();
312
+ newControls.pollingQAControl = { enabled: true };
313
+ locusInfo.updateControls(newControls);
314
+
315
+ assert.calledWith(
316
+ locusInfo.emitScoped,
317
+ {file: 'locus-info', function: 'updateControls'},
318
+ LOCUSINFO.EVENTS.CONTROLS_POLLING_QA_CHANGED,
319
+ {state: newControls.pollingQAControl}
320
+ );
321
+ });
308
322
 
309
323
  it('should keep the recording state to `IDLE`', () => {
310
324
  locusInfo.controls = {
@@ -102,6 +102,8 @@ describe('plugin-meetings', () => {
102
102
  canEnableRemoteDesktopControl: null,
103
103
  canDisableRemoteDesktopControl: null,
104
104
  canMoveToLobby: null,
105
+ canEnablePollingQA: null,
106
+ canDisablePollingQA: null,
105
107
 
106
108
  ...expected,
107
109
  };
@@ -210,6 +212,8 @@ describe('plugin-meetings', () => {
210
212
  'canEnableRemoteDesktopControl',
211
213
  'canDisableRemoteDesktopControl',
212
214
  'canMoveToLobby',
215
+ 'canEnablePollingQA',
216
+ 'canDisablePollingQA',
213
217
  ].forEach((key) => {
214
218
  it(`get and set for ${key} work as expected`, () => {
215
219
  const inMeetingActions = new InMeetingActions();