@webex/plugin-meetings 3.9.0-next.14 → 3.9.0-next.15

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.
@@ -294,7 +294,7 @@ export default class LocusInfo extends EventsScope {
294
294
  this.updateLocusCache(locus);
295
295
  // above section only updates the locusInfo object
296
296
  // The below section makes sure it updates the locusInfo as well as updates the meeting object
297
- this.updateParticipants(locus.participants);
297
+ this.updateParticipants(locus.participants, []);
298
298
  // For 1:1 space meeting the conversation Url does not exist in locus.conversation
299
299
  this.updateConversationUrl(locus.conversationUrl, locus.info);
300
300
  this.updateControls(locus.controls, locus.self);
@@ -335,6 +335,8 @@ export default class LocusInfo extends EventsScope {
335
335
  const locus = this.getTheLocusToUpdate(data.locus);
336
336
  LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
337
337
 
338
+ locus.jsSdkMeta = {removedParticipantIds: []};
339
+
338
340
  switch (eventType) {
339
341
  case LOCUSEVENT.PARTICIPANT_JOIN:
340
342
  case LOCUSEVENT.PARTICIPANT_LEFT:
@@ -400,7 +402,11 @@ export default class LocusInfo extends EventsScope {
400
402
  this.participants = locus.participants;
401
403
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
402
404
  this.updateLocusInfo(locus);
403
- this.updateParticipants(locus.participants, isReplaceMembers);
405
+ this.updateParticipants(
406
+ locus.participants,
407
+ locus.jsSdkMeta?.removedParticipantIds,
408
+ isReplaceMembers
409
+ );
404
410
  this.isMeetingActive();
405
411
  this.handleOneOnOneEvent(eventType);
406
412
  this.updateEmbeddedApps(locus.embeddedApps);
@@ -462,7 +468,11 @@ export default class LocusInfo extends EventsScope {
462
468
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
463
469
  this.mergeParticipants(this.participants, locus.participants);
464
470
  this.updateLocusInfo(locus);
465
- this.updateParticipants(locus.participants, isReplaceMembers);
471
+ this.updateParticipants(
472
+ locus.participants,
473
+ locus.jsSdkMeta?.removedParticipantIds,
474
+ isReplaceMembers
475
+ );
466
476
  this.isMeetingActive();
467
477
  }
468
478
 
@@ -753,11 +763,12 @@ export default class LocusInfo extends EventsScope {
753
763
  /**
754
764
  * update meeting's members
755
765
  * @param {Object} participants new participants object
766
+ * @param {Array} removedParticipantIds list of removed participants
756
767
  * @param {Boolean} isReplace is replace the whole members
757
768
  * @returns {Array} updatedParticipants
758
769
  * @memberof LocusInfo
759
770
  */
760
- updateParticipants(participants: object, isReplace?: boolean) {
771
+ updateParticipants(participants: object, removedParticipantIds?: string[], isReplace?: boolean) {
761
772
  this.emitScoped(
762
773
  {
763
774
  file: 'locus-info',
@@ -766,6 +777,7 @@ export default class LocusInfo extends EventsScope {
766
777
  EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS,
767
778
  {
768
779
  participants,
780
+ removedParticipantIds,
769
781
  recordingId: this.parsedLocus.controls && this.parsedLocus.controls.record?.modifiedBy,
770
782
  selfIdentity: this.parsedLocus.self && this.parsedLocus.self.selfIdentity,
771
783
  selfId: this.parsedLocus.self && this.parsedLocus.self.selfId,
@@ -109,4 +109,5 @@ export interface Participant {
109
109
  status: ParticipantMediaStatus;
110
110
  type: string;
111
111
  url: ParticipantUrl;
112
+ isRemoved: boolean; // JS-SDK internal field to indicate in updates when the participant is removed
112
113
  }
@@ -39,6 +39,17 @@ export default class MembersCollection {
39
39
  return this.members;
40
40
  }
41
41
 
42
+ /**
43
+ * Removes a member from the collection
44
+ * @param {String} id
45
+ * @returns {void}
46
+ */
47
+ remove(id: string) {
48
+ if (this.members[id]) {
49
+ delete this.members[id];
50
+ }
51
+ }
52
+
42
53
  /**
43
54
  * @returns {void}
44
55
  * reset members
@@ -74,7 +74,11 @@ import {Invitee} from '../meeting/type';
74
74
  * @memberof Members
75
75
  */
76
76
 
77
- type UpdatedMembers = {added: Array<Member>; updated: Array<Member>};
77
+ type UpdatedMembers = {
78
+ added: Array<Member>;
79
+ updated: Array<Member>;
80
+ removedIds?: Array<string>; // removed member ids
81
+ };
78
82
  /**
79
83
  * @class Members
80
84
  */
@@ -384,12 +388,18 @@ export default class Members extends StatelessWebexPlugin {
384
388
  * when new participant updates come in, both delta and full participants, update them in members collection
385
389
  * delta object in the event will have {updated, added} and full will be the full membersCollection
386
390
  * @param {Object} payload
387
- * @param {Object} payload.participants
391
+ * @param {Object} payload.participants new/updated participants
392
+ * @param {Boolean} payload.isReplace whether to replace the whole members collection
393
+ * @param {Object} payload.removedParticipantIds ids of the removed participants
388
394
  * @returns {undefined}
389
395
  * @private
390
396
  * @memberof Members
391
397
  */
392
- locusParticipantsUpdate(payload: {participants: object; isReplace?: boolean}) {
398
+ locusParticipantsUpdate(payload: {
399
+ participants: object;
400
+ isReplace?: boolean;
401
+ removedParticipantIds?: Array<string>;
402
+ }) {
393
403
  if (payload) {
394
404
  if (payload.isReplace) {
395
405
  this.clearMembers();
@@ -547,10 +557,22 @@ export default class Members extends StatelessWebexPlugin {
547
557
  private handleMembersUpdate(membersUpdate: UpdatedMembers) {
548
558
  this.constructMembers(membersUpdate.updated, true);
549
559
  this.constructMembers(membersUpdate.added, false);
560
+ this.removeMembers(membersUpdate.removedIds);
550
561
 
551
562
  return this.membersCollection.getAll();
552
563
  }
553
564
 
565
+ /**
566
+ * removes members from the collection
567
+ * @param {Array<string>} removedMembers removed members ids
568
+ * @returns {void}
569
+ */
570
+ private removeMembers(removedMembers: Array<string>) {
571
+ removedMembers.forEach((memberId) => {
572
+ this.membersCollection.remove(memberId);
573
+ });
574
+ }
575
+
554
576
  /**
555
577
  * set members to the member collection from each updated/added lists as passed in
556
578
  * @param {Array} list
@@ -600,6 +622,10 @@ export default class Members extends StatelessWebexPlugin {
600
622
  }
601
623
  const memberUpdate = this.update(payload.participants);
602
624
 
625
+ // this code depends on memberIds being the same as participantIds
626
+ // if MemberUtil.extractId() ever changes, this will need to be updated
627
+ memberUpdate.removedIds = payload.removedParticipantIds || [];
628
+
603
629
  return memberUpdate;
604
630
  }
605
631
 
@@ -772,7 +772,7 @@ describe('plugin-meetings', () => {
772
772
  },
773
773
  };
774
774
  locusInfo.emitScoped = sinon.stub();
775
- locusInfo.updateParticipants({});
775
+ locusInfo.updateParticipants({}, []);
776
776
 
777
777
  // if this assertion fails, double-check the attributes used in
778
778
  // the updateParticipants function in locus-info/index.js
@@ -790,6 +790,7 @@ describe('plugin-meetings', () => {
790
790
  selfId: '2',
791
791
  hostId: '3',
792
792
  isReplace: undefined,
793
+ removedParticipantIds: [],
793
794
  }
794
795
  );
795
796
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -814,7 +815,7 @@ describe('plugin-meetings', () => {
814
815
  };
815
816
 
816
817
  locusInfo.emitScoped = sinon.stub();
817
- locusInfo.updateParticipants({}, true);
818
+ locusInfo.updateParticipants({}, [], true);
818
819
 
819
820
  assert.calledWith(
820
821
  locusInfo.emitScoped,
@@ -830,6 +831,7 @@ describe('plugin-meetings', () => {
830
831
  selfId: '2',
831
832
  hostId: '3',
832
833
  isReplace: true,
834
+ removedParticipantIds: [],
833
835
  }
834
836
  );
835
837
  });
@@ -847,7 +849,7 @@ describe('plugin-meetings', () => {
847
849
  ];
848
850
 
849
851
  locusInfo.emitScoped = sinon.stub();
850
- locusInfo.updateParticipants(failureParticipant);
852
+ locusInfo.updateParticipants(failureParticipant, []);
851
853
  assert.calledWith(
852
854
  locusInfo.emitScoped,
853
855
  {
@@ -2050,7 +2052,7 @@ describe('plugin-meetings', () => {
2050
2052
 
2051
2053
  fakeLocus = {
2052
2054
  meeting: true,
2053
- participants: true,
2055
+ participants: [],
2054
2056
  url: 'newLocusUrl',
2055
2057
  syncUrl: 'newSyncUrl',
2056
2058
  };
@@ -2525,7 +2527,7 @@ describe('plugin-meetings', () => {
2525
2527
  });
2526
2528
 
2527
2529
  it('onDeltaLocus handle delta data', () => {
2528
- fakeLocus.participants = {};
2530
+ fakeLocus.participants = [];
2529
2531
  const fakeBreakout = {
2530
2532
  sessionId: 'sessionId',
2531
2533
  groupId: 'groupId',
@@ -2542,11 +2544,11 @@ describe('plugin-meetings', () => {
2542
2544
  };
2543
2545
  locusInfo.updateParticipants = sinon.stub();
2544
2546
  locusInfo.onDeltaLocus(fakeLocus);
2545
- assert.calledWith(locusInfo.updateParticipants, {}, false);
2547
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, false);
2546
2548
 
2547
2549
  fakeLocus.controls.breakout.sessionId = 'sessionId2';
2548
2550
  locusInfo.onDeltaLocus(fakeLocus);
2549
- assert.calledWith(locusInfo.updateParticipants, {}, true);
2551
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, true);
2550
2552
  });
2551
2553
 
2552
2554
  it('onDeltaLocus merges delta participants with existing participants', () => {
@@ -2563,7 +2565,7 @@ describe('plugin-meetings', () => {
2563
2565
  existingParticipants,
2564
2566
  FAKE_DELTA_PARTICIPANTS
2565
2567
  );
2566
- assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2568
+ assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, undefined, false);
2567
2569
  });
2568
2570
 
2569
2571
  [true, false].forEach((isDelta) =>
@@ -3074,6 +3076,7 @@ describe('plugin-meetings', () => {
3074
3076
  id: 'test person id',
3075
3077
  },
3076
3078
  },
3079
+ participants: [],
3077
3080
  });
3078
3081
 
3079
3082
  updateLocusInfoStub.resetHistory();
@@ -0,0 +1,120 @@
1
+ const { default: MembersCollection } = require("../../../../src/members/collection");
2
+ const { default: Member } = require("../../../../src/member");
3
+ const { assert } = require('@webex/test-helper-chai');
4
+
5
+
6
+ describe('plugin-meetings', () => {
7
+ describe('MembersCollection', () => {
8
+ let collection;
9
+ let member1;
10
+ let member2;
11
+ const participant1 = { controls: {}, status: {} };
12
+ const participant2 = { controls: {}, status: {} };
13
+
14
+ beforeEach(() => {
15
+ collection = new MembersCollection();
16
+ member1 = new Member(participant1);
17
+ member2 = new Member(participant2);
18
+ });
19
+
20
+ it('starts with no members', () => {
21
+ assert.equal(Object.keys(collection.getAll()).length, 0);
22
+ });
23
+
24
+ it('can add members', () => {
25
+ collection.set('member1', member1);
26
+ assert.equal(Object.keys(collection.getAll()).length, 1);
27
+ assert.equal(collection.get('member1'), member1);
28
+
29
+ collection.set('member2', member2);
30
+ assert.equal(Object.keys(collection.getAll()).length, 2);
31
+ assert.equal(collection.get('member2'), member2);
32
+ });
33
+
34
+ it('can remove members', () => {
35
+ // Add some members first
36
+ collection.set('member1', member1);
37
+ collection.set('member2', member2);
38
+ assert.equal(Object.keys(collection.getAll()).length, 2);
39
+
40
+ // Remove one member
41
+ collection.remove('member1');
42
+ assert.equal(Object.keys(collection.getAll()).length, 1);
43
+ assert.equal(collection.get('member1'), undefined);
44
+ assert.equal(collection.get('member2'), member2);
45
+
46
+ // Remove another member
47
+ collection.remove('member2');
48
+ assert.equal(Object.keys(collection.getAll()).length, 0);
49
+ assert.equal(collection.get('member2'), undefined);
50
+
51
+ // Removing non-existent member should not cause errors
52
+ collection.remove('nonExistent');
53
+ assert.equal(Object.keys(collection.getAll()).length, 0);
54
+ });
55
+
56
+ describe('reset', () => {
57
+ it('removes all members', () => {
58
+ // Add some members first
59
+ collection.set('member1', member1);
60
+ collection.set('member2', member2);
61
+ assert.equal(Object.keys(collection.getAll()).length, 2);
62
+
63
+ // Reset should clear all members
64
+ collection.reset();
65
+ assert.equal(Object.keys(collection.getAll()).length, 0);
66
+ assert.equal(collection.get('member1'), undefined);
67
+ assert.equal(collection.get('member2'), undefined);
68
+ });
69
+ });
70
+
71
+ describe('setAll', () => {
72
+ it('replaces all members with new collection', () => {
73
+ // Add initial member
74
+ collection.set('member1', member1);
75
+ assert.equal(Object.keys(collection.getAll()).length, 1);
76
+
77
+ // Set all with new collection
78
+ const newMembers = {
79
+ 'member2': member2,
80
+ 'member3': new Member(participant1)
81
+ };
82
+ collection.setAll(newMembers);
83
+
84
+ assert.equal(Object.keys(collection.getAll()).length, 2);
85
+ assert.equal(collection.get('member1'), undefined);
86
+ assert.equal(collection.get('member2'), member2);
87
+ assert.exists(collection.get('member3'));
88
+ });
89
+ });
90
+
91
+ describe('get', () => {
92
+ it('returns undefined for non-existent members', () => {
93
+ assert.equal(collection.get('nonExistent'), undefined);
94
+ });
95
+
96
+ it('returns correct member for existing id', () => {
97
+ collection.set('member1', member1);
98
+ assert.equal(collection.get('member1'), member1);
99
+ });
100
+ });
101
+
102
+ describe('getAll', () => {
103
+ it('returns empty object when no members', () => {
104
+ const allMembers = collection.getAll();
105
+ assert.isObject(allMembers);
106
+ assert.equal(Object.keys(allMembers).length, 0);
107
+ });
108
+
109
+ it('returns all members', () => {
110
+ collection.set('member1', member1);
111
+ collection.set('member2', member2);
112
+
113
+ const allMembers = collection.getAll();
114
+ assert.equal(Object.keys(allMembers).length, 2);
115
+ assert.equal(allMembers.member1, member1);
116
+ assert.equal(allMembers.member2, member2);
117
+ });
118
+ });
119
+ });
120
+ });
@@ -320,9 +320,18 @@ describe('plugin-meetings', () => {
320
320
  EVENT_TRIGGERS.MEMBERS_CLEAR,
321
321
  {}
322
322
  );
323
+ sinon.restore();
323
324
  });
324
325
  });
325
326
  describe('#locusParticipantsUpdate', () => {
327
+ beforeEach(() => {
328
+ sinon.stub(Trigger, 'trigger');
329
+ });
330
+
331
+ afterEach(() => {
332
+ sinon.restore();
333
+ });
334
+
326
335
  it('should send member update event with session info', () => {
327
336
  const members = createMembers({url: url1});
328
337
  const fakePayload = {
@@ -343,13 +352,45 @@ describe('plugin-meetings', () => {
343
352
  },
344
353
  EVENT_TRIGGERS.MEMBERS_UPDATE,
345
354
  {
346
- delta: {added: [], updated: []},
355
+ delta: {added: [], updated: [], removedIds: []},
347
356
  full: {},
348
357
  isReplace: true,
349
358
  }
350
359
  );
351
360
  });
352
361
 
362
+ it('should handle participants being removed', () => {
363
+ const members = createMembers({url: url1});
364
+
365
+ // setup the collection with a fake member
366
+ members.membersCollection.setAll(fakeMembersCollection);
367
+ assert.equal(Object.keys(members.membersCollection.getAll()).length, 1);
368
+
369
+ // remove the member
370
+ members.locusParticipantsUpdate({
371
+ participants: [],
372
+ removedParticipantIds: ['test1'],
373
+ });
374
+
375
+ assert.equal(Object.keys(members.membersCollection.getAll()).length, 0);
376
+
377
+ // check that the event was emitted
378
+ assert.calledWith(
379
+ Trigger.trigger,
380
+ members,
381
+ {
382
+ file: 'members',
383
+ function: 'locusParticipantsUpdate',
384
+ },
385
+ EVENT_TRIGGERS.MEMBERS_UPDATE,
386
+ {
387
+ delta: {added: [], updated: [], removedIds: ['test1']},
388
+ full: {},
389
+ isReplace: false,
390
+ }
391
+ );
392
+ });
393
+
353
394
  describe('handles members with paired devices correctly', () => {
354
395
  const runCheck = (propsForUpdate, expectedPropsOnPairedMember) => {
355
396
  const members = createMembers({url: url1});