@webex/plugin-meetings 3.0.0-beta.66 → 3.0.0-beta.67

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 (47) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +11 -1
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +7 -2
  5. package/dist/constants.js.map +1 -1
  6. package/dist/locus-info/controlsUtils.js +15 -0
  7. package/dist/locus-info/controlsUtils.js.map +1 -1
  8. package/dist/locus-info/index.js +9 -6
  9. package/dist/locus-info/index.js.map +1 -1
  10. package/dist/meeting/index.js +1 -0
  11. package/dist/meeting/index.js.map +1 -1
  12. package/dist/meetings/collection.js +22 -0
  13. package/dist/meetings/collection.js.map +1 -1
  14. package/dist/meetings/index.js +91 -11
  15. package/dist/meetings/index.js.map +1 -1
  16. package/dist/meetings/util.js +32 -0
  17. package/dist/meetings/util.js.map +1 -1
  18. package/dist/members/collection.js +10 -0
  19. package/dist/members/collection.js.map +1 -1
  20. package/dist/members/index.js +21 -1
  21. package/dist/members/index.js.map +1 -1
  22. package/dist/types/constants.d.ts +3 -0
  23. package/dist/types/locus-info/index.d.ts +3 -3
  24. package/dist/types/meeting/index.d.ts +1 -0
  25. package/dist/types/meetings/collection.d.ts +8 -0
  26. package/dist/types/meetings/index.d.ts +18 -0
  27. package/dist/types/members/collection.d.ts +5 -0
  28. package/dist/types/members/index.d.ts +8 -0
  29. package/package.json +18 -18
  30. package/src/breakouts/index.ts +13 -0
  31. package/src/constants.ts +3 -0
  32. package/src/locus-info/controlsUtils.ts +19 -0
  33. package/src/locus-info/index.ts +8 -5
  34. package/src/meeting/index.ts +1 -0
  35. package/src/meetings/collection.ts +20 -0
  36. package/src/meetings/index.ts +110 -2
  37. package/src/meetings/util.ts +38 -0
  38. package/src/members/collection.ts +8 -0
  39. package/src/members/index.ts +24 -1
  40. package/test/unit/spec/breakouts/breakout.ts +3 -2
  41. package/test/unit/spec/breakouts/index.ts +10 -0
  42. package/test/unit/spec/locus-info/controlsUtils.js +21 -0
  43. package/test/unit/spec/locus-info/index.js +63 -0
  44. package/test/unit/spec/meetings/collection.js +14 -0
  45. package/test/unit/spec/meetings/index.js +183 -6
  46. package/test/unit/spec/meetings/utils.js +46 -0
  47. package/test/unit/spec/members/index.js +38 -0
@@ -40,4 +40,24 @@ export default class MeetingCollection extends Collection {
40
40
 
41
41
  return null;
42
42
  }
43
+
44
+ /**
45
+ * get a specific meeting searching for key
46
+ * @param {String} breakoutUrl
47
+ * @returns {Meeting} if found, else returns null
48
+ * @public
49
+ * @memberof MeetingCollection
50
+ */
51
+ public getActiveBreakoutLocus(breakoutUrl: string) {
52
+ if (breakoutUrl) {
53
+ // @ts-ignore
54
+ return find(
55
+ // @ts-ignore
56
+ this.meetings,
57
+ (meeting) => meeting.breakouts?.url === breakoutUrl && meeting.breakouts?.isActiveBreakout
58
+ );
59
+ }
60
+
61
+ return null;
62
+ }
43
63
  }
@@ -40,6 +40,10 @@ import {
40
40
  MEETING_REMOVED_REASON,
41
41
  _CONVERSATION_URL_,
42
42
  CONVERSATION_URL,
43
+ MEETINGNUMBER,
44
+ BREAKOUTS,
45
+ _JOINED_,
46
+ _MOVED_,
43
47
  } from '../constants';
44
48
  import BEHAVIORAL_METRICS from '../metrics/constants';
45
49
  import MeetingInfo from '../meeting-info';
@@ -227,6 +231,103 @@ export default class Meetings extends WebexPlugin {
227
231
  this.onReady();
228
232
  }
229
233
 
234
+ /**
235
+ * check whether you need to handle this main session's locus data or not
236
+ * @param {Object} meeting current meeting data
237
+ * @param {Object} newLocus new locus data
238
+ * @returns {boolean}
239
+ * @private
240
+ * @memberof Meetings
241
+ */
242
+ private isNeedHandleMainLocus(meeting: any, newLocus: any) {
243
+ const breakoutUrl = newLocus.controls?.breakout?.url;
244
+ const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
245
+
246
+ const isSelfJoined = newLocus?.self?.state === _JOINED_;
247
+ const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
248
+ const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus);
249
+ const isNewLocusJoinThisDevice = MeetingsUtil.joinedOnThisDevice(meeting, newLocus);
250
+ const isBreakoutLocusJoinThisDevice =
251
+ breakoutLocus?.joinedWith?.correlationId &&
252
+ breakoutLocus.joinedWith.correlationId === meeting?.correlationId;
253
+
254
+ if (isSelfJoined && isNewLocusJoinThisDevice) {
255
+ LoggerProxy.logger.log(
256
+ 'Meetings:index#isNeedHandleMainLocus --> self this device shown as JOINED in the main session'
257
+ );
258
+ if (breakoutLocus?.joinedWith && deviceFromNewLocus) {
259
+ const breakoutReplaceAt =
260
+ breakoutLocus.joinedWith.replaces?.length > 0
261
+ ? breakoutLocus.joinedWith.replaces[0].replaceAt
262
+ : '';
263
+ const newLocusReplaceAt =
264
+ deviceFromNewLocus.replaces?.length > 0 ? deviceFromNewLocus.replaces[0].replaceAt : '';
265
+ if (breakoutReplaceAt && newLocusReplaceAt && breakoutReplaceAt > newLocusReplaceAt) {
266
+ LoggerProxy.logger.log(
267
+ `Meetings:index#isNeedHandleMainLocus --> this is expired main joined status locus_dto replacedAt ${newLocusReplaceAt} bo replacedAt ${breakoutReplaceAt}`
268
+ );
269
+
270
+ return false;
271
+ }
272
+ }
273
+
274
+ return true;
275
+ }
276
+ if (isBreakoutLocusJoinThisDevice) {
277
+ LoggerProxy.logger.log(
278
+ `Meetings:index#isNeedHandleMainLocus --> there is active breakout session and joined on this device, and don't need to handle main session: ${breakoutUrl}`
279
+ );
280
+
281
+ return false;
282
+ }
283
+ if (isSelfMoved && newLocus?.self?.removed) {
284
+ LoggerProxy.logger.log(
285
+ 'Meetings:index#isNeedHandleMainLocus --> self moved main locus with self removed status, not need to handle'
286
+ );
287
+
288
+ return false;
289
+ }
290
+ LoggerProxy.logger.log(
291
+ 'Meetings:index#isNeedHandleMainLocus --> this is a normal main session locusDTO update case'
292
+ );
293
+
294
+ return true;
295
+ }
296
+
297
+ /**
298
+ * check whether you need to handle this locus data or not
299
+ * @param {Object} meeting old locus data
300
+ * @param {Object} newLocus new locus data
301
+ * @returns {boolean}
302
+ * @private
303
+ * @memberof Meetings
304
+ */
305
+ private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
306
+ if (newLocus) {
307
+ const isNewLocusAsBreakout =
308
+ newLocus.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT;
309
+ const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
310
+ if (!meeting) {
311
+ if (isNewLocusAsBreakout) {
312
+ LoggerProxy.logger.log(
313
+ `Meetings:index#isNeedHandleLocusDTO --> the first breakout session locusDTO active status: ${newLocus.fullState?.active}`
314
+ );
315
+
316
+ return newLocus.self?.state === _JOINED_;
317
+ }
318
+
319
+ return this.isNeedHandleMainLocus(meeting, newLocus);
320
+ }
321
+ if (!isNewLocusAsBreakout) {
322
+ return this.isNeedHandleMainLocus(meeting, newLocus);
323
+ }
324
+
325
+ return !isSelfMoved;
326
+ }
327
+
328
+ return true;
329
+ }
330
+
230
331
  /**
231
332
  * handle locus events and takes meeting actions with them as they come in
232
333
  * @param {Object} data a locus event
@@ -240,7 +341,6 @@ export default class Meetings extends WebexPlugin {
240
341
  */
241
342
  private handleLocusEvent(data: {locusUrl: string; locus: any}, useRandomDelayForInfo = false) {
242
343
  let meeting = null;
243
-
244
344
  // getting meeting by correlationId. This will happen for the new event
245
345
  // Either the locus
246
346
  // TODO : Add check for the callBack Address
@@ -260,7 +360,8 @@ export default class Meetings extends WebexPlugin {
260
360
  ) ||
261
361
  (data.locus.info?.isUnifiedSpaceMeeting
262
362
  ? undefined
263
- : this.meetingCollection.getByKey(CONVERSATION_URL, data.locus.conversationUrl));
363
+ : this.meetingCollection.getByKey(CONVERSATION_URL, data.locus.conversationUrl)) ||
364
+ this.meetingCollection.getByKey(MEETINGNUMBER, data.locus?.info?.webExMeetingId);
264
365
 
265
366
  // Special case when locus has got replaced, This only happend once if a replace locus exists
266
367
  // https://sqbu-github.cisco.com/WebExSquared/locus/wiki/Locus-changing-mid-call
@@ -273,6 +374,13 @@ export default class Meetings extends WebexPlugin {
273
374
  );
274
375
  }
275
376
 
377
+ if (!this.isNeedHandleLocusDTO(meeting, data.locus)) {
378
+ LoggerProxy.logger.log(
379
+ `Meetings:index#handleLocusEvent --> doesn't need to process locus event`
380
+ );
381
+
382
+ return;
383
+ }
276
384
  if (!meeting) {
277
385
  // TODO: create meeting when we get a meeting object
278
386
  // const checkForEnded = (locus) => {
@@ -8,6 +8,9 @@ import {
8
8
  CORRELATION_ID,
9
9
  EVENT_TRIGGERS,
10
10
  ROAP,
11
+ _LEFT_,
12
+ _MOVED_,
13
+ _JOINED_,
11
14
  } from '../constants';
12
15
  import LoggerProxy from '../common/logs/logger-proxy';
13
16
  import Trigger from '../common/events/trigger-proxy';
@@ -220,4 +223,39 @@ MeetingsUtil.checkH264Support = async function checkH264Support(options: {
220
223
  }, delay);
221
224
  };
222
225
 
226
+ /**
227
+ * get device from locus data
228
+ * @param {Object} newLocus new locus data
229
+ * @returns {Object}
230
+ */
231
+ MeetingsUtil.getThisDevice = (newLocus: any) => {
232
+ if (newLocus?.self?.devices?.length > 0) {
233
+ const [thisDevice] = newLocus.self.devices;
234
+
235
+ return thisDevice;
236
+ }
237
+
238
+ return null;
239
+ };
240
+
241
+ /**
242
+ * get self device joined status from locus data
243
+ * @param {Object} meeting current meeting data
244
+ * @param {Object} newLocus new locus data
245
+ * @returns {Object}
246
+ */
247
+ MeetingsUtil.joinedOnThisDevice = (meeting: any, newLocus: any) => {
248
+ const thisDevice = MeetingsUtil.getThisDevice(newLocus);
249
+ if (thisDevice) {
250
+ if (!thisDevice.correlationId || meeting?.correlationId === thisDevice.correlationId) {
251
+ return (
252
+ thisDevice.state === _JOINED_ ||
253
+ (thisDevice.state === _LEFT_ && thisDevice.reason === _MOVED_)
254
+ );
255
+ }
256
+ }
257
+
258
+ return false;
259
+ };
260
+
223
261
  export default MeetingsUtil;
@@ -37,4 +37,12 @@ export default class MembersCollection {
37
37
  getAll() {
38
38
  return this.members;
39
39
  }
40
+
41
+ /**
42
+ * @returns {void}
43
+ * reset members
44
+ */
45
+ reset() {
46
+ this.members = {};
47
+ }
40
48
  }
@@ -283,6 +283,25 @@ export default class Members extends StatelessWebexPlugin {
283
283
  );
284
284
  }
285
285
 
286
+ /**
287
+ * clear member collection
288
+ * @returns {void}
289
+ * @private
290
+ * @memberof Members
291
+ */
292
+ clearMembers() {
293
+ this.membersCollection.reset();
294
+ Trigger.trigger(
295
+ this,
296
+ {
297
+ file: 'members',
298
+ function: 'clearMembers',
299
+ },
300
+ EVENT_TRIGGERS.MEMBERS_CLEAR,
301
+ {}
302
+ );
303
+ }
304
+
286
305
  /**
287
306
  * when new participant updates come in, both delta and full participants, update them in members collection
288
307
  * delta object in the event will have {updated, added} and full will be the full membersCollection
@@ -292,8 +311,11 @@ export default class Members extends StatelessWebexPlugin {
292
311
  * @private
293
312
  * @memberof Members
294
313
  */
295
- locusParticipantsUpdate(payload: {participants: object}) {
314
+ locusParticipantsUpdate(payload: {participants: object; isReplace?: boolean}) {
296
315
  if (payload) {
316
+ if (payload.isReplace) {
317
+ this.clearMembers();
318
+ }
297
319
  const delta = this.handleLocusInfoUpdatedParticipants(payload);
298
320
  const full = this.handleMembersUpdate(delta); // SDK should propagate the full list for both delta and non delta updates
299
321
 
@@ -309,6 +331,7 @@ export default class Members extends StatelessWebexPlugin {
309
331
  {
310
332
  delta,
311
333
  full,
334
+ isReplace: !!payload.isReplace,
312
335
  }
313
336
  );
314
337
  }
@@ -20,6 +20,7 @@ describe('plugin-meetings', () => {
20
20
  breakout = new Breakout({}, {parent: breakouts});
21
21
  breakout.groupId = 'groupId';
22
22
  breakout.sessionId = 'sessionId';
23
+ breakout.sessionType = 'BREAKOUT';
23
24
  breakout.url = 'url';
24
25
  webex.request = sinon.stub().returns(Promise.resolve('REQUEST_RETURN_VALUE'));
25
26
  });
@@ -140,7 +141,7 @@ describe('plugin-meetings', () => {
140
141
 
141
142
  assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate, locusData);
142
143
  assert.equal(result, undefined);
143
- });
144
- });
144
+ })
145
+ })
145
146
  });
146
147
  });
@@ -292,6 +292,16 @@ describe('plugin-meetings', () => {
292
292
  });
293
293
  });
294
294
 
295
+ describe('#isActiveBreakout', () => {
296
+ it('return is current is breakout with active status', () => {
297
+ assert.equal(breakouts.isActiveBreakout, false);
298
+ breakouts.set('sessionType', BREAKOUTS.SESSION_TYPES.BREAKOUT);
299
+ assert.equal(breakouts.isActiveBreakout, false);
300
+ breakouts.set('status', BREAKOUTS.STATUS.OPEN);
301
+ assert.equal(breakouts.isActiveBreakout, true);
302
+ });
303
+ });
304
+
295
305
  describe('#queryRosters', () => {
296
306
  it('makes the expected query', async () => {
297
307
  webex.request.returns(
@@ -1,5 +1,6 @@
1
1
  import {assert} from '@webex/test-helper-chai';
2
2
  import ControlsUtils from '@webex/plugin-meetings/src/locus-info/controlsUtils';
3
+ import controlsUtils from "@webex/plugin-meetings/src/locus-info/controlsUtils";
3
4
 
4
5
  const defaultControls = {
5
6
  entryExitTone: {
@@ -156,5 +157,25 @@ describe('plugin-meetings', () => {
156
157
  });
157
158
  });
158
159
  });
160
+
161
+ describe('isNeedReplaceMembers', () => {
162
+ it('if no breakout control, return false', () => {
163
+ const oldControls = {};
164
+ const newControls = {};
165
+ assert.equal(controlsUtils.isNeedReplaceMembers(oldControls, newControls), false);
166
+ });
167
+
168
+ it('if current session moved, return true', () => {
169
+ const oldControls = {breakout: {sessionId: 'sessionId1', groupId: 'groupId1'}};
170
+ const newControls = {breakout: {sessionId: 'sessionId2', groupId: 'groupId2'}};
171
+ assert.equal(controlsUtils.isNeedReplaceMembers(oldControls, newControls), true);
172
+ });
173
+
174
+ it('if in same session, return false', () => {
175
+ const oldControls = {breakout: {sessionId: 'sessionId1', groupId: 'groupId'}};
176
+ const newControls = {breakout: {sessionId: 'sessionId1', groupId: 'groupId'}};
177
+ assert.equal(controlsUtils.isNeedReplaceMembers(oldControls, newControls), false);
178
+ });
179
+ })
159
180
  });
160
181
  });
@@ -504,6 +504,7 @@ describe('plugin-meetings', () => {
504
504
  selfIdentity: '123',
505
505
  selfId: '2',
506
506
  hostId: '3',
507
+ isReplace: undefined,
507
508
  }
508
509
  );
509
510
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -511,6 +512,43 @@ describe('plugin-meetings', () => {
511
512
  // are being correctly grabbed from locusInfo.parsedLocus within updateParticipants
512
513
  });
513
514
 
515
+ it('should call with breakout control info', () => {
516
+ locusInfo.parsedLocus = {
517
+ controls: {
518
+ record: {
519
+ modifiedBy: '1',
520
+ },
521
+ },
522
+ self: {
523
+ selfIdentity: '123',
524
+ selfId: '2',
525
+ },
526
+ host: {
527
+ hostId: '3',
528
+ },
529
+ };
530
+
531
+ locusInfo.emitScoped = sinon.stub();
532
+ locusInfo.updateParticipants({}, true);
533
+
534
+ assert.calledWith(
535
+ locusInfo.emitScoped,
536
+ {
537
+ file: 'locus-info',
538
+ function: 'updateParticipants',
539
+ },
540
+ EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS,
541
+ {
542
+ participants: {},
543
+ recordingId: '1',
544
+ selfIdentity: '123',
545
+ selfId: '2',
546
+ hostId: '3',
547
+ isReplace: true,
548
+ }
549
+ );
550
+ });
551
+
514
552
  it('should update the deltaParticipants object', () => {
515
553
  const prev = locusInfo.deltaParticipants;
516
554
 
@@ -1444,6 +1482,31 @@ describe('plugin-meetings', () => {
1444
1482
  assert.calledOnce(locusInfo.locusParser.resume);
1445
1483
  });
1446
1484
  });
1485
+
1486
+ it('onDeltaLocus handle delta data', () => {
1487
+ fakeLocus.participants = {};
1488
+ const fakeBreakout = {
1489
+ sessionId: 'sessionId',
1490
+ groupId: 'groupId',
1491
+ };
1492
+
1493
+ fakeLocus.controls = {
1494
+ breakout: fakeBreakout
1495
+ };
1496
+ locusInfo.controls = {
1497
+ breakout: {
1498
+ sessionId: 'sessionId',
1499
+ groupId: 'groupId',
1500
+ }
1501
+ }
1502
+ locusInfo.updateParticipants = sinon.stub();
1503
+ locusInfo.onDeltaLocus(fakeLocus);
1504
+ assert.calledWith(locusInfo.updateParticipants, {}, false);
1505
+
1506
+ fakeBreakout.sessionId = 'sessionId2';
1507
+ locusInfo.onDeltaLocus(fakeLocus);
1508
+ assert.calledWith(locusInfo.updateParticipants, {}, false);
1509
+ });
1447
1510
  });
1448
1511
 
1449
1512
  describe('#handleOneonOneEvent', () => {
@@ -48,5 +48,19 @@ describe('plugin-meetings', () => {
48
48
  assert.deepEqual(meetingCollection.getByKey('value', 'test'), {value: 'test', id: uuid1});
49
49
  });
50
50
  });
51
+
52
+ describe('#getActiveBreakoutLocus', () => {
53
+ beforeEach(() => {
54
+ meetingCollection.meetings.test = {breakouts: {url: 'url', isActiveBreakout: true}, id: uuid1};
55
+ });
56
+ it('return null if empty breakoutUrl', () => {
57
+ assert.deepEqual(meetingCollection.getActiveBreakoutLocus(), null);
58
+ });
59
+
60
+ it('should get the meeting which joined breakout by breakoutUrl', () => {
61
+ assert.deepEqual(meetingCollection.getActiveBreakoutLocus('url'), {
62
+ breakouts: {url: 'url', isActiveBreakout: true}, id: uuid1});
63
+ });
64
+ });
51
65
  });
52
66
  });