@webex/plugin-meetings 3.8.0-next.82 → 3.8.0-next.84

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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
- import {isEmpty} from 'lodash';
4
+ import {get, isEmpty, set} from 'lodash';
5
5
  // @ts-ignore
6
6
  import {StatelessWebexPlugin} from '@webex/webex-core';
7
7
 
@@ -73,6 +73,7 @@ import {ServerRoleShape} from './types';
73
73
  * @memberof Members
74
74
  */
75
75
 
76
+ type UpdatedMembers = {added: Array<Member>; updated: Array<Member>};
76
77
  /**
77
78
  * @class Members
78
79
  */
@@ -81,7 +82,7 @@ export default class Members extends StatelessWebexPlugin {
81
82
  locusUrl: any;
82
83
  mediaShareContentId: any;
83
84
  mediaShareWhiteboardId: any;
84
- membersCollection: any;
85
+ membersCollection: MembersCollection;
85
86
  membersRequest: any;
86
87
  receiveSlotManager: ReceiveSlotManager;
87
88
  mediaRequestManagers: {
@@ -321,6 +322,63 @@ export default class Members extends StatelessWebexPlugin {
321
322
  );
322
323
  }
323
324
 
325
+ /**
326
+ * Updates properties on members that rely on information from other members.
327
+ * This function MUST be called only after the membersCollection has been fully updated
328
+ * @param {UpdatedMembers} membersUpdate
329
+ * @returns {Object} membersCollection
330
+ * @private
331
+ * @memberof Members
332
+ */
333
+ private updateRelationsBetweenMembers(membersUpdate: UpdatedMembers) {
334
+ const updatePairedMembers = (membersList: Member[]) => {
335
+ membersList.forEach((member) => {
336
+ if (!member.pairedWith.participantUrl) {
337
+ // if we don't have a participantUrl set, it may be that we had it in the past and not anymore, so cleanup the rest of the data
338
+ if (member.pairedWith.memberId) {
339
+ const pairedMember = this.membersCollection.get(member.pairedWith.memberId);
340
+
341
+ if (pairedMember) {
342
+ // remove member from pairedMember's associatedUsers array
343
+ pairedMember.associatedUsers.delete(member.id);
344
+
345
+ if (pairedMember.associatedUser === member.id) {
346
+ pairedMember.associatedUser = null;
347
+ }
348
+
349
+ // reset all the props that we set on pairedMember
350
+ pairedMember.isPairedWithSelf = false;
351
+ pairedMember.isHost = false;
352
+ }
353
+ }
354
+ member.pairedWith.memberId = undefined;
355
+ } else if (member.pairedWith.memberId === undefined) {
356
+ // we have participantUrl set but not memberId, so find the member and set it
357
+ const pairedMember = Object.values(this.membersCollection.getAll()).find(
358
+ (m) => m.participant?.url === member.pairedWith.participantUrl
359
+ );
360
+
361
+ if (pairedMember) {
362
+ member.pairedWith.memberId = pairedMember.id;
363
+ pairedMember.associatedUsers.add(member.id);
364
+
365
+ if (pairedMember.associatedUsers.size === 1) {
366
+ // associatedUser is deprecated, because it's broken - device can have multiple associated users,
367
+ // so for backwards compatibility we set it to the first associated user
368
+ pairedMember.associatedUser = member.id;
369
+ }
370
+
371
+ pairedMember.isPairedWithSelf = member.isSelf;
372
+ pairedMember.isHost = member.isHost;
373
+ }
374
+ }
375
+ });
376
+ };
377
+
378
+ updatePairedMembers(membersUpdate.updated);
379
+ updatePairedMembers(membersUpdate.added);
380
+ }
381
+
324
382
  /**
325
383
  * when new participant updates come in, both delta and full participants, update them in members collection
326
384
  * delta object in the event will have {updated, added} and full will be the full membersCollection
@@ -338,6 +396,8 @@ export default class Members extends StatelessWebexPlugin {
338
396
  const delta = this.handleLocusInfoUpdatedParticipants(payload);
339
397
  const full = this.handleMembersUpdate(delta); // SDK should propagate the full list for both delta and non delta updates
340
398
 
399
+ this.updateRelationsBetweenMembers(delta);
400
+
341
401
  this.receiveSlotManager?.updateMemberIds();
342
402
 
343
403
  Trigger.trigger(
@@ -478,20 +538,14 @@ export default class Members extends StatelessWebexPlugin {
478
538
 
479
539
  /**
480
540
  * sets values in the members collection for updated and added properties from delta
481
- * @param {Object} membersUpdate {updated: [], added: []}
541
+ * @param {UpdatedMembers} membersUpdate
482
542
  * @returns {Object} membersCollection
483
543
  * @private
484
544
  * @memberof Members
485
545
  */
486
- private handleMembersUpdate(membersUpdate: any) {
487
- if (membersUpdate) {
488
- if (membersUpdate.updated) {
489
- this.constructMembers(membersUpdate.updated);
490
- }
491
- if (membersUpdate.added) {
492
- this.constructMembers(membersUpdate.added);
493
- }
494
- }
546
+ private handleMembersUpdate(membersUpdate: UpdatedMembers) {
547
+ this.constructMembers(membersUpdate.updated, true);
548
+ this.constructMembers(membersUpdate.added, false);
495
549
 
496
550
  return this.membersCollection.getAll();
497
551
  }
@@ -499,12 +553,30 @@ export default class Members extends StatelessWebexPlugin {
499
553
  /**
500
554
  * set members to the member collection from each updated/added lists as passed in
501
555
  * @param {Array} list
556
+ * @param {boolean} isUpdate
502
557
  * @returns {undefined}
503
558
  * @private
504
559
  * @memberof Members
505
560
  */
506
- private constructMembers(list: Array<any>) {
561
+ private constructMembers(list: Array<any>, isUpdate: boolean) {
507
562
  list.forEach((member) => {
563
+ if (isUpdate) {
564
+ // some member props are generated by SDK and need to be preserved on update,
565
+ // because they depend on relationships with other members so they need to be handled
566
+ // at the end, once all members are updated - this is done in updateRelationsBetweenMembers()
567
+ const propsToKeepOnUpdate = ['pairedWith.memberId'];
568
+
569
+ const existingMember = this.membersCollection.get(member.id);
570
+ if (existingMember) {
571
+ propsToKeepOnUpdate.forEach((prop) => {
572
+ const existingValue = get(existingMember, prop);
573
+
574
+ if (existingValue !== undefined) {
575
+ set(member, prop, existingValue);
576
+ }
577
+ });
578
+ }
579
+ }
508
580
  this.membersCollection.set(member.id, member);
509
581
  });
510
582
  }
@@ -512,11 +584,11 @@ export default class Members extends StatelessWebexPlugin {
512
584
  /**
513
585
  * Internal update the participants value
514
586
  * @param {Object} payload
515
- * @returns {Object}
587
+ * @returns {UpdatedMembers}
516
588
  * @private
517
589
  * @memberof Members
518
590
  */
519
- private handleLocusInfoUpdatedParticipants(payload: any) {
591
+ private handleLocusInfoUpdatedParticipants(payload: any): UpdatedMembers {
520
592
  this.hostId = payload.hostId || this.hostId;
521
593
  this.selfId = payload.selfId || this.selfId;
522
594
  this.recordingId = payload.recordingId;
@@ -681,12 +753,12 @@ export default class Members extends StatelessWebexPlugin {
681
753
  * Removed/left members will end up in updates
682
754
  * Each array contains only members
683
755
  * @param {Array} participants the locus participants
684
- * @returns {Object} {added: {Array}, updated: {Array}}
756
+ * @returns {UpdatedMembers} {added: {Array}, updated: {Array}}
685
757
  * @private
686
758
  * @memberof Members
687
759
  */
688
- private update(participants: Array<any>) {
689
- const membersUpdate = {added: [], updated: []};
760
+ private update(participants: Array<any>): UpdatedMembers {
761
+ const membersUpdate: UpdatedMembers = {added: [], updated: []};
690
762
 
691
763
  if (participants) {
692
764
  participants.forEach((participant) => {
@@ -32,58 +32,7 @@ sinon.assert.expose(chai.assert, {prefix: ''});
32
32
  describe('plugin-meetings', () => {
33
33
  let webex;
34
34
  let url1;
35
- const fakeMembersCollection = {
36
- test1: {
37
- namespace: 'Meetings',
38
- participant: {
39
- state: 'JOINED',
40
- type: 'USER',
41
- person: {
42
- id: '6eb08f8b-bf69-3251-a126-b161bead2d21',
43
- phoneNumber: '+18578675309',
44
- isExternal: true,
45
- primaryDisplayString: '+18578675309',
46
- },
47
- devices: [
48
- {
49
- url: 'https://fakeURL.com',
50
- deviceType: 'SIP',
51
- state: 'JOINED',
52
- intents: [null],
53
- correlationId: '1234',
54
- provisionalUrl: 'dialout:///fake',
55
- isSparkPstn: true,
56
- },
57
- {
58
- url: 'dialout:///fakeagain',
59
- deviceType: 'PROVISIONAL',
60
- state: 'JOINED',
61
- intents: [null],
62
- correlationId: '4321',
63
- isVideoCallback: false,
64
- clientUrl: 'https://fakeURL',
65
- provisionalType: 'DIAL_OUT_ONLY',
66
- dialingStatus: 'SUCCESS',
67
- },
68
- ],
69
- status: {
70
- audioStatus: 'SENDRECV',
71
- videoStatus: 'INACTIVE',
72
- },
73
- id: 'abc-123-abc-123',
74
- guest: true,
75
- resourceGuest: false,
76
- moderator: false,
77
- panelist: false,
78
- moveToLobbyNotAllowed: true,
79
- deviceUrl: 'https://fakeDeviceurl',
80
- },
81
- id: 'abc-123-abc-123',
82
- status: 'IN_MEETING',
83
- type: 'MEETING',
84
- isModerator: false,
85
- },
86
- };
35
+ let fakeMembersCollection;
87
36
 
88
37
  describe('members', () => {
89
38
  const sandbox = sinon.createSandbox();
@@ -92,6 +41,65 @@ describe('plugin-meetings', () => {
92
41
  let membersRequestSpy;
93
42
 
94
43
  beforeEach(() => {
44
+ fakeMembersCollection = {
45
+ test1: {
46
+ associatedUsers: new Set(),
47
+ namespace: 'Meetings',
48
+ participant: {
49
+ state: 'JOINED',
50
+ type: 'USER',
51
+ person: {
52
+ id: '6eb08f8b-bf69-3251-a126-b161bead2d21',
53
+ phoneNumber: '+18578675309',
54
+ isExternal: true,
55
+ primaryDisplayString: '+18578675309',
56
+ },
57
+ devices: [
58
+ {
59
+ url: 'https://fakeURL.com',
60
+ deviceType: 'SIP',
61
+ state: 'JOINED',
62
+ intents: [null],
63
+ correlationId: '1234',
64
+ provisionalUrl: 'dialout:///fake',
65
+ isSparkPstn: true,
66
+ },
67
+ {
68
+ url: 'dialout:///fakeagain',
69
+ deviceType: 'PROVISIONAL',
70
+ state: 'JOINED',
71
+ intents: [null],
72
+ correlationId: '4321',
73
+ isVideoCallback: false,
74
+ clientUrl: 'https://fakeURL',
75
+ provisionalType: 'DIAL_OUT_ONLY',
76
+ dialingStatus: 'SUCCESS',
77
+ },
78
+ ],
79
+ status: {
80
+ audioStatus: 'SENDRECV',
81
+ videoStatus: 'INACTIVE',
82
+ },
83
+ id: 'test1',
84
+ guest: true,
85
+ resourceGuest: false,
86
+ moderator: false,
87
+ panelist: false,
88
+ moveToLobbyNotAllowed: true,
89
+ deviceUrl: 'https://fakeDeviceurl',
90
+ url: 'fake participant url for test1',
91
+ },
92
+ id: 'test1',
93
+ status: 'IN_MEETING',
94
+ type: 'USER',
95
+ isModerator: false,
96
+ isHost: false,
97
+ isSelf: false,
98
+ isContentSharing: false,
99
+ pairedWith: {},
100
+ },
101
+ };
102
+
95
103
  webex = new MockWebex({
96
104
  children: {
97
105
  meetings: Meetings,
@@ -291,6 +299,110 @@ describe('plugin-meetings', () => {
291
299
  }
292
300
  );
293
301
  });
302
+
303
+ describe('handles members with paired devices correctly', () => {
304
+ const runCheck = (propsForUpdate, expectedPropsOnPairedMember) => {
305
+ const members = createMembers({url: url1});
306
+
307
+ const DEVICE_PARTICIPANT_URL = 'fake participant url for test2';
308
+
309
+ members.membersCollection.setAll(fakeMembersCollection);
310
+
311
+ // simulate a locus update with a member that has a paired device
312
+ members.locusParticipantsUpdate({
313
+ ...propsForUpdate,
314
+ participants: [
315
+ {
316
+ id: 'test1',
317
+ type: 'USER',
318
+ person: {},
319
+ devices: [
320
+ {
321
+ intents: [
322
+ {
323
+ type: 'OBSERVE',
324
+ associatedWith: DEVICE_PARTICIPANT_URL,
325
+ },
326
+ ],
327
+ },
328
+ ],
329
+ },
330
+ {
331
+ id: 'test2',
332
+ type: 'RESOURCE_ROOM',
333
+ person: {},
334
+ devices: [
335
+ {
336
+ state: 'JOINED',
337
+ intents: [null],
338
+ },
339
+ ],
340
+ url: DEVICE_PARTICIPANT_URL,
341
+ },
342
+ ],
343
+ });
344
+
345
+ let member = members.membersCollection.get('test1');
346
+ assert.isDefined(member.pairedWith);
347
+ assert.strictEqual(member.pairedWith.participantUrl, DEVICE_PARTICIPANT_URL);
348
+ assert.strictEqual(member.pairedWith.memberId, 'test2');
349
+
350
+ let pairedDeviceMember = members.membersCollection.get('test2');
351
+ assert(pairedDeviceMember.associatedUsers.has(member.id));
352
+ assert.strictEqual(pairedDeviceMember.associatedUser, member.id);
353
+ assert.strictEqual(pairedDeviceMember.associatedUsers.size, 1);
354
+
355
+ assert.strictEqual(
356
+ pairedDeviceMember.isPairedWithSelf,
357
+ expectedPropsOnPairedMember.isPairedWithSelf
358
+ );
359
+ assert.strictEqual(pairedDeviceMember.isHost, expectedPropsOnPairedMember.isHost);
360
+
361
+ // now simulate the user and paired device leaving the meeting
362
+ members.locusParticipantsUpdate({
363
+ ...propsForUpdate,
364
+ participants: [
365
+ {
366
+ id: 'test1',
367
+ type: 'USER',
368
+ person: {},
369
+ devices: [],
370
+ },
371
+ {
372
+ id: 'test2',
373
+ type: 'RESOURCE_ROOM',
374
+ person: {},
375
+ devices: [],
376
+ },
377
+ ],
378
+ });
379
+
380
+ // and check that all the relevant properties were reset
381
+ member = members.membersCollection.get('test1');
382
+ assert.isDefined(member.pairedWith);
383
+ assert.isUndefined(member.pairedWith.participantUrl);
384
+ assert.isUndefined(member.pairedWith.memberId);
385
+
386
+ pairedDeviceMember = members.membersCollection.get('test2');
387
+ assert.strictEqual(pairedDeviceMember.associatedUser, null);
388
+ assert.strictEqual(pairedDeviceMember.associatedUsers.size, 0);
389
+
390
+ assert.strictEqual(pairedDeviceMember.isPairedWithSelf, false);
391
+ assert.strictEqual(pairedDeviceMember.isHost, false);
392
+ };
393
+
394
+ it('sets the right properties when a member has a paired device', () => {
395
+ runCheck({}, {isPairedWithSelf: false, isHost: false});
396
+ });
397
+
398
+ it('sets the right properties when a member has a paired device (isSelf)', () => {
399
+ runCheck({selfId: 'test1'}, {isPairedWithSelf: true, isHost: false});
400
+ });
401
+
402
+ it('sets the right properties when a member has a paired device (isHost)', () => {
403
+ runCheck({hostId: 'test1'}, {isPairedWithSelf: false, isHost: true});
404
+ });
405
+ });
294
406
  });
295
407
  describe('#sendDialPadKey', () => {
296
408
  it('should throw a rejection when calling sendDialPadKey with no tones', async () => {