@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.61

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 (158) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +6 -3
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -371,6 +371,7 @@ const MeetingUtil = {
371
371
  meeting.breakouts.cleanUp();
372
372
  meeting.webinar.cleanUp();
373
373
  meeting.simultaneousInterpretation.cleanUp();
374
+ meeting.locusInfo.cleanUp();
374
375
  meeting.locusMediaRequest = undefined;
375
376
 
376
377
  meeting.webex?.internal?.newMetrics?.callDiagnosticMetrics?.clearEventLimitsForCorrelationId(
@@ -845,6 +846,19 @@ const MeetingUtil = {
845
846
  requestBody.sequence = sequence;
846
847
  },
847
848
 
849
+ /**
850
+ * Checks if Locus API response contains a Locus DTO
851
+ *
852
+ * @param {any} response http response from Locus API call
853
+ * @returns {boolean} true if response contains a Locus DTO
854
+ */
855
+ isLocusDtoInAPIResponse(response: any) {
856
+ return (
857
+ response?.body?.locus || // for APIs called on our participant - locus is one of props in the response body
858
+ response?.body?.url // for APIs that act on locus itself (like mute all), the body is the locus
859
+ );
860
+ },
861
+
848
862
  /**
849
863
  * Updates the locus info for the meeting with the locus
850
864
  * information returned from API requests made to Locus
@@ -853,12 +867,13 @@ const MeetingUtil = {
853
867
  * @param {Object} response The response of the http request
854
868
  * @returns {Object}
855
869
  */
856
- updateLocusFromApiResponse: (meeting, response) => {
870
+ updateLocusFromApiResponse: (meeting: any, response: any) => {
857
871
  if (!meeting) {
858
872
  return response;
859
873
  }
860
874
 
861
- if (response?.body?.locus) {
875
+ // locus API responses can come in different shapes:
876
+ if (MeetingUtil.isLocusDtoInAPIResponse(response)) {
862
877
  meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
863
878
  }
864
879
 
@@ -930,6 +945,9 @@ const MeetingUtil = {
930
945
  attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
931
946
  displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
932
947
 
948
+ isAnonymizeDisplayNamesEnabled: (displayHints) =>
949
+ displayHints.includes(DISPLAY_HINTS.ANONYMOUS_DISPLAY_NAMES_ENABLED),
950
+
933
951
  selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
934
952
  if (!userPolicies) {
935
953
  return true;
@@ -55,10 +55,12 @@ import PasswordError from '../common/errors/password-error';
55
55
  import CaptchaError from '../common/errors/captcha-error';
56
56
  import MeetingCollection from './collection';
57
57
  import {
58
+ FetchSitePreferencesMeViaSiteOptions,
58
59
  MEETING_KEY,
59
60
  INoiseReductionEffect,
60
61
  IVirtualBackgroundEffect,
61
62
  MeetingRegistrationStatus,
63
+ SitePreferencesResponse,
62
64
  } from './meetings.types';
63
65
  import MeetingsUtil from './util';
64
66
  import PermissionError from '../common/errors/permission';
@@ -69,7 +71,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
71
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
72
  import {HashTreeObject} from '../hashTree/types';
71
73
  import {isSelf} from '../hashTree/utils';
74
+
72
75
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
76
+ import {LocusDTO} from '../locus-info/types';
73
77
 
74
78
  let mediaLogger;
75
79
 
@@ -313,7 +317,7 @@ export default class Meetings extends WebexPlugin {
313
317
  const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
314
318
 
315
319
  const isSelfJoined = newLocus?.self?.state === _JOINED_;
316
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
320
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
317
321
  // @ts-ignore
318
322
  const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus, this.webex.internal.device.url);
319
323
  const isResourceMovedOnThisDevice =
@@ -390,7 +394,7 @@ export default class Meetings extends WebexPlugin {
390
394
  private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
391
395
  if (newLocus) {
392
396
  const isNewLocusAsBreakout = MeetingsUtil.isBreakoutLocusDTO(newLocus);
393
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
397
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
394
398
  const isSelfMovedToLobby =
395
399
  newLocus?.self?.devices[0]?.intent?.reason === _ON_HOLD_LOBBY_ &&
396
400
  newLocus?.self?.devices[0]?.intent?.type === _WAIT_;
@@ -435,14 +439,11 @@ export default class Meetings extends WebexPlugin {
435
439
  if (existingMeeting) {
436
440
  return existingMeeting;
437
441
  }
438
-
439
442
  if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
443
  // need to check if maybe this event indicates a move to/from breakout
441
444
  const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
- data.stateElementsMessage,
443
- this.meetingCollection,
444
- // @ts-ignore
445
- this.webex.internal.device.url
445
+ data?.stateElementsMessage,
446
+ this.meetingCollection
446
447
  );
447
448
 
448
449
  if (meetingForHashTreeMessage) {
@@ -492,7 +493,6 @@ export default class Meetings extends WebexPlugin {
492
493
  */
493
494
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
494
495
  let meeting = this.getCorrespondingMeetingByLocus(data);
495
-
496
496
  // @ts-ignore
497
497
  if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
498
  storeEventForDebugging('mercury', data);
@@ -586,17 +586,21 @@ export default class Meetings extends WebexPlugin {
586
586
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
587
587
  .then(async (newMeeting) => {
588
588
  meeting = newMeeting;
589
-
590
589
  try {
591
590
  // It's a new meeting so initialize the locus data
592
- await meeting.locusInfo.initialSetup({
593
- trigger:
594
- data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
- ? 'get-loci-response'
596
- : 'locus-message',
597
- locus: data.locus,
598
- hashTreeMessage: data.stateElementsMessage,
599
- });
591
+ await meeting.locusInfo.initialSetup(
592
+ {
593
+ trigger:
594
+ data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
+ ? 'get-loci-response'
596
+ : 'locus-message',
597
+ locus: data.locus,
598
+ hashTreeMessage: data.stateElementsMessage,
599
+ },
600
+ (locus: LocusDTO) => {
601
+ meeting.finalizeMeetingAfterInitialLocusSetup(locus);
602
+ }
603
+ );
600
604
  } catch (error) {
601
605
  LoggerProxy.logger.warn(
602
606
  `Meetings:index#handleLocusEvent --> Error initializing locus data: ${error.message}`
@@ -931,6 +935,27 @@ export default class Meetings extends WebexPlugin {
931
935
  }
932
936
  }
933
937
 
938
+ /**
939
+ * API to toggle AV1 codec support for video slides in multistream,
940
+ * needs to be called before webex.meetings.joinWithMedia()
941
+ *
942
+ * @param {Boolean} newValue
943
+ * @private
944
+ * @memberof Meetings
945
+ * @returns {undefined}
946
+ */
947
+ private _toggleEnableAv1SlidesSupport(newValue: boolean) {
948
+ if (typeof newValue !== 'boolean') {
949
+ return;
950
+ }
951
+
952
+ // @ts-ignore
953
+ if (this.config.enableAv1SlidesSupport !== newValue) {
954
+ // @ts-ignore
955
+ this.config.enableAv1SlidesSupport = newValue;
956
+ }
957
+ }
958
+
934
959
  /**
935
960
  * API to toggle stopping ICE Candidates Gathering after first relay candidate,
936
961
  * needs to be called before webex.meetings.joinWithMedia()
@@ -1403,6 +1428,31 @@ export default class Meetings extends WebexPlugin {
1403
1428
  return this.personalMeetingRoom;
1404
1429
  }
1405
1430
 
1431
+ /**
1432
+ * Fetches site preferences for the provided Webex site, or the preferred Webex site.
1433
+ * This is used to determine capabilities of the site, such as whether scheduling a webinar is supported.
1434
+ *
1435
+ * @param {object} [options]
1436
+ * @param {string} [options.siteUrl] - Webex site URL. Defaults to preferredWebexSite, for example "cisco.webex.com".
1437
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from siteUrl, for example "cisco" for "cisco.webex.com".
1438
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
1439
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
1440
+ * @throws {ParameterError}
1441
+ * @public
1442
+ * @memberof Meetings
1443
+ * @example
1444
+ * const preferences = await webex.meetings.fetchSitePreferencesMeViaSite();
1445
+ * const supportScheduleWebinar = preferences?.scheduling?.supportScheduleWebinar;
1446
+ */
1447
+ public fetchSitePreferencesMeViaSite(
1448
+ options: FetchSitePreferencesMeViaSiteOptions = {}
1449
+ ): Promise<SitePreferencesResponse> {
1450
+ return this.request.fetchSitePreferencesMeViaSite({
1451
+ ...options,
1452
+ siteUrl: options.siteUrl || this.preferredWebexSite,
1453
+ });
1454
+ }
1455
+
1406
1456
  /**
1407
1457
  * Returns basic information about a meeting that exists or
1408
1458
  * used to exist in the MeetingCollection
@@ -1765,10 +1815,14 @@ export default class Meetings extends WebexPlugin {
1765
1815
  extraParams: infoExtraParams,
1766
1816
  sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1767
1817
  };
1818
+ const shouldDeferMeetingInfoFetch = type === DESTINATION_TYPE.LOCUS_ID && !destination?.info;
1819
+
1820
+ const isOneOnOneCallLocus =
1821
+ type === DESTINATION_TYPE.LOCUS_ID && MeetingsUtil.isOneOnOneCall(destination);
1768
1822
 
1769
1823
  if (meetingInfo) {
1770
1824
  meeting.injectMeetingInfo(meetingInfo, meetingInfoOptions, meetingLookupUrl);
1771
- } else if (type !== DESTINATION_TYPE.ONE_ON_ONE_CALL) {
1825
+ } else if (type !== DESTINATION_TYPE.ONE_ON_ONE_CALL && !isOneOnOneCallLocus) {
1772
1826
  // ignore fetchMeetingInfo for 1:1 meetings
1773
1827
  if (enableUnifiedMeetings && !isMeetingActive && useRandomDelayForInfo && waitingTime > 0) {
1774
1828
  meeting.fetchMeetingInfoTimeoutId = setTimeout(
@@ -1776,8 +1830,12 @@ export default class Meetings extends WebexPlugin {
1776
1830
  waitingTime
1777
1831
  );
1778
1832
  meeting.parseMeetingInfo(undefined, destination);
1779
- } else {
1833
+ } else if (!shouldDeferMeetingInfoFetch) {
1780
1834
  await meeting.fetchMeetingInfo(meetingInfoOptions);
1835
+ } else {
1836
+ LoggerProxy.logger.info(
1837
+ 'Meetings:index#createMeeting --> defer fetchMeetingInfo for incomplete locus, will do it after locus initialSetup'
1838
+ );
1781
1839
  }
1782
1840
  }
1783
1841
  } catch (err) {
@@ -1811,7 +1869,11 @@ export default class Meetings extends WebexPlugin {
1811
1869
  // For type LOCUS_ID we need to parse the locus object to get the information
1812
1870
  // about the caller and callee
1813
1871
  // Meeting Added event will be created in `handleLocusEvent`
1814
- if (type !== DESTINATION_TYPE.LOCUS_ID) {
1872
+ // Only emit MEETING_ADDED if the meeting still exists in the collection.
1873
+ // If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
1874
+ // skip emitting to prevent orphaned meeting references on the consumer side.
1875
+ // @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
1876
+ if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
1815
1877
  if (!meeting.sipUri) {
1816
1878
  meeting.setSipUri(destination);
1817
1879
  }
@@ -1886,23 +1948,23 @@ export default class Meetings extends WebexPlugin {
1886
1948
  * @public
1887
1949
  * @memberof Meetings
1888
1950
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1951
+ public async syncMeetings({
1952
+ keepOnlyLocusMeetings = true,
1953
+ skipHashTreeSync = false,
1954
+ } = {}): Promise<void> {
1890
1955
  // @ts-ignore
1891
1956
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1957
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1958
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1959
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray) => {
1902
- const activeLocusUrl = [];
1960
+ } else {
1961
+ try {
1962
+ const locusArray = await this.request.getActiveMeetings();
1963
+ const activeLocusUrl: string[] = [];
1903
1964
 
1904
1965
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1966
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1967
+
1906
1968
  lociToUpdate.forEach((locus) => {
1907
1969
  activeLocusUrl.push(locus.url);
1908
1970
  this.handleLocusEvent({
@@ -1920,21 +1982,50 @@ export default class Meetings extends WebexPlugin {
1920
1982
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1983
  for (const meeting of Object.values(meetingsCollection)) {
1922
1984
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1985
+ const {locusUrl, locusInfo} = meeting;
1924
1986
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1987
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1988
+
1989
+ if (
1990
+ globalMeetingId &&
1991
+ locusArray?.loci?.some(
1992
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1993
+ )
1994
+ ) {
1995
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1996
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1997
+ // is in a breakout and gets moved to a different breakout while we were offline
1998
+ } else {
1999
+ // destroy function also uploads logs
2000
+ // @ts-ignore
2001
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
2002
+ }
1928
2003
  }
1929
2004
  }
1930
2005
  }
1931
- })
1932
- .catch((error) => {
2006
+ } catch (error) {
1933
2007
  LoggerProxy.logger.error(
1934
2008
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
2009
  );
1936
- throw new Error(error);
1937
- });
2010
+ throw error;
2011
+ }
2012
+ }
2013
+
2014
+ if (!skipHashTreeSync) {
2015
+ // Trigger hash tree syncs for all remaining meetings
2016
+ const remainingMeetings = this.meetingCollection.getAll();
2017
+ const syncPromises = [];
2018
+
2019
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
2020
+ if (meeting.locusInfo) {
2021
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
2022
+ }
2023
+ }
2024
+
2025
+ if (syncPromises.length > 0) {
2026
+ await Promise.all(syncPromises);
2027
+ }
2028
+ }
1938
2029
  }
1939
2030
 
1940
2031
  /**
@@ -1950,8 +2041,8 @@ export default class Meetings extends WebexPlugin {
1950
2041
  this.breakoutLocusForHandleLater = [];
1951
2042
  const lociToUpdate = [...mainLoci];
1952
2043
  breakoutLoci.forEach((breakoutLocus) => {
1953
- const associateMainLocus = mainLoci.find(
1954
- (mainLocus) => mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
2044
+ const associateMainLocus = mainLoci.find((mainLocus) =>
2045
+ MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus)
1955
2046
  );
1956
2047
  const existCorrespondingMeeting = this.getCorrespondingMeetingByLocus({
1957
2048
  eventType: LOCUSEVENT.SDK_NO_EVENT,
@@ -1979,7 +2070,7 @@ export default class Meetings extends WebexPlugin {
1979
2070
  * @public
1980
2071
  * @memberof Meetings
1981
2072
  */
1982
- checkHandleBreakoutLocus(newCreatedLocus) {
2073
+ checkHandleBreakoutLocus(newCreatedLocus: any) {
1983
2074
  if (
1984
2075
  !newCreatedLocus ||
1985
2076
  !this.breakoutLocusForHandleLater ||
@@ -1990,9 +2081,8 @@ export default class Meetings extends WebexPlugin {
1990
2081
  if (MeetingsUtil.isBreakoutLocusDTO(newCreatedLocus)) {
1991
2082
  return;
1992
2083
  }
1993
- const existIndex = this.breakoutLocusForHandleLater.findIndex(
1994
- (breakoutLocus) =>
1995
- breakoutLocus.controls?.breakout?.url === newCreatedLocus.controls?.breakout?.url
2084
+ const existIndex = this.breakoutLocusForHandleLater.findIndex((breakoutLocus: any) =>
2085
+ MeetingsUtil.isMainAssociatedWithBreakout(newCreatedLocus, breakoutLocus)
1996
2086
  );
1997
2087
 
1998
2088
  if (existIndex < 0) {
@@ -31,3 +31,22 @@ export type MeetingRegistrationStatus = {
31
31
  mercuryConnect: boolean;
32
32
  checkH264Support: boolean;
33
33
  };
34
+
35
+ export enum SitePreferenceSelectOption {
36
+ SCHEDULING = 'scheduling',
37
+ }
38
+
39
+ export type FetchSitePreferencesMeViaSiteOptions = {
40
+ siteUrl?: string;
41
+ siteName?: string;
42
+ selectOptions?: SitePreferenceSelectOption[];
43
+ };
44
+
45
+ export const DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS = [SitePreferenceSelectOption.SCHEDULING];
46
+
47
+ export type SitePreferencesResponse = {
48
+ scheduling?: {
49
+ supportScheduleWebinar?: boolean;
50
+ webinarWebLink?: string;
51
+ };
52
+ };
@@ -2,7 +2,14 @@
2
2
  import {StatelessWebexPlugin} from '@webex/webex-core';
3
3
 
4
4
  import LoggerProxy from '../common/logs/logger-proxy';
5
+ import ParameterError from '../common/errors/parameter';
5
6
  import {HTTP_VERBS, API, RESOURCE} from '../constants';
7
+ import {
8
+ DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS,
9
+ type FetchSitePreferencesMeViaSiteOptions,
10
+ type SitePreferencesResponse,
11
+ } from './meetings.types';
12
+ import MeetingsUtil from './util';
6
13
 
7
14
  /**
8
15
  * @class MeetingRequest
@@ -45,6 +52,42 @@ export default class MeetingRequest extends StatelessWebexPlugin {
45
52
  return this.webex.internal.services.getMeetingPreferences();
46
53
  }
47
54
 
55
+ /**
56
+ * Fetches site preferences from a given site given a select option and a siteUrl with an optional siteName. If siteName is not provided, it will be derived from the siteUrl. If siteUrl is not provided, it will throw an error. If selectOptions is not provided, it will default to scheduling.
57
+ *
58
+ * @param {object} [options]
59
+ * @param {string} [options.siteUrl] - Webex site URL, for example "cisco.webex.com".
60
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from options.siteUrl, e.g., "cisco".
61
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
62
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
63
+ * @throws {ParameterError}
64
+ * @public
65
+ * @memberof MeetingRequest
66
+ */
67
+ fetchSitePreferencesMeViaSite(
68
+ options: FetchSitePreferencesMeViaSiteOptions = {}
69
+ ): Promise<SitePreferencesResponse> {
70
+ const {siteUrl, selectOptions = DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS} = options;
71
+
72
+ if (!siteUrl) {
73
+ throw new ParameterError(
74
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
75
+ );
76
+ }
77
+
78
+ // @ts-ignore - config comes from registerPlugin
79
+ const multipartSitePrefixList = this.config.meetings.multipartSitePrefixList || [];
80
+ const siteName = options.siteName || MeetingsUtil.getSiteName(siteUrl, multipartSitePrefixList);
81
+
82
+ // @ts-ignore
83
+ return this.request({
84
+ method: HTTP_VERBS.GET,
85
+ uri: `https://${siteUrl}/wbxappapi/v1/users/me/preference?select=${encodeURIComponent(
86
+ selectOptions.join(',')
87
+ )}&siteurl=${encodeURIComponent(siteName)}`,
88
+ }).then((res: any) => res.body);
89
+ }
90
+
48
91
  // locus federation, determines and populate locus if the responseBody has remote URLs to fetch locus details
49
92
 
50
93
  /**
@@ -1,12 +1,15 @@
1
1
  /* globals window */
2
2
 
3
3
  import {
4
+ _CALL_,
4
5
  _CREATED_,
5
6
  _INCOMING_,
6
7
  _JOINED_,
7
8
  _LEFT_,
8
9
  DESTINATION_TYPE,
9
10
  _MOVED_,
11
+ _SIP_BRIDGE_,
12
+ _SPACE_SHARE_,
10
13
  BREAKOUTS,
11
14
  EVENT_TRIGGERS,
12
15
  LOCUS,
@@ -18,6 +21,7 @@ import Trigger from '../common/events/trigger-proxy';
18
21
  import BEHAVIORAL_METRICS from '../metrics/constants';
19
22
  import Metrics from '../metrics';
20
23
  import {MEETING_KEY} from './meetings.types';
24
+ import {EndMeetingReason, LocusFullState} from '../locus-info/types';
21
25
 
22
26
  /**
23
27
  * Meetings Media Codec Missing Event
@@ -152,6 +156,30 @@ MeetingsUtil.parseDefaultSiteFromMeetingPreferences = (userPreferences) => {
152
156
  return result;
153
157
  };
154
158
 
159
+ MeetingsUtil.getSiteName = (site: string, multipartSitePrefixList: string[] = []) => {
160
+ if (!site) {
161
+ return null;
162
+ }
163
+
164
+ let siteName: string | undefined;
165
+
166
+ multipartSitePrefixList.forEach((multipartSitePrefix) => {
167
+ if (!siteName && site.includes(multipartSitePrefix)) {
168
+ const secondDot = site.indexOf('.', site.indexOf('.') + 1);
169
+
170
+ siteName = site.substring(0, secondDot);
171
+ }
172
+ });
173
+
174
+ if (siteName) {
175
+ return siteName;
176
+ }
177
+
178
+ siteName = site.substring(0, site.indexOf('.'));
179
+
180
+ return siteName;
181
+ };
182
+
155
183
  /**
156
184
  * Will check to see if the H.264 media codec is supported.
157
185
  * @async
@@ -266,6 +294,49 @@ MeetingsUtil.getThisDevice = (newLocus: any, deviceUrl: string) => {
266
294
  return null;
267
295
  };
268
296
 
297
+ /**
298
+ * Checks if the fullState indicates the meeting has fully ended (not just a breakout move).
299
+ * @param {Object} fullState locus fullState data
300
+ * @returns {boolean}
301
+ */
302
+ MeetingsUtil.isWholeMeetingEnded = (fullState: LocusFullState): boolean => {
303
+ return (
304
+ fullState.state === LOCUS.STATE.INACTIVE &&
305
+ fullState.endMeetingReason !== EndMeetingReason.breakoutEnded
306
+ );
307
+ };
308
+
309
+ /**
310
+ * Checks if the self state in a locus indicates a breakout move or breakout end.
311
+ * Returns true when:
312
+ * - self state is LEFT with reason MOVED (regular breakout move), OR
313
+ * - fullState is INACTIVE with endMeetingReason BREAKOUT_ENDED (breakout session ended)
314
+ * @param {Object} locus locus data
315
+ * @returns {boolean}
316
+ */
317
+ MeetingsUtil.isSelfMovedOrBreakoutEnded = (locus: any): boolean => {
318
+ const isSelfLeftMoved = locus?.self?.state === _LEFT_ && locus?.self?.reason === _MOVED_;
319
+ const isBreakoutEnded =
320
+ locus?.fullState?.state === LOCUS.STATE.INACTIVE &&
321
+ locus?.fullState?.endMeetingReason === EndMeetingReason.breakoutEnded;
322
+
323
+ return isSelfLeftMoved || isBreakoutEnded;
324
+ };
325
+
326
+ /**
327
+ * Checks if a locus is a 1:1 call using locus.fullState.type.
328
+ * Returns true when fullState.type is CALL, SIP_BRIDGE, or SPACE_SHARE.
329
+ * @param {Object} locus locus data
330
+ * @returns {boolean}
331
+ */
332
+ MeetingsUtil.isOneOnOneCall = (locus: any): boolean => {
333
+ const fullStateType = locus?.fullState?.type;
334
+
335
+ return (
336
+ fullStateType === _CALL_ || fullStateType === _SIP_BRIDGE_ || fullStateType === _SPACE_SHARE_
337
+ );
338
+ };
339
+
269
340
  /**
270
341
  * get self device joined status from locus data
271
342
  * @param {Object} meeting current meeting data
@@ -294,7 +365,10 @@ MeetingsUtil.joinedOnThisDevice = (meeting: any, newLocus: any, deviceUrl: strin
294
365
  * @private
295
366
  */
296
367
  MeetingsUtil.isBreakoutLocusDTO = (newLocus: any) => {
297
- return newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT;
368
+ return (
369
+ newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT ||
370
+ !!newLocus?.info?.isBreakout
371
+ );
298
372
  };
299
373
 
300
374
  /**
@@ -310,4 +384,26 @@ MeetingsUtil.isValidBreakoutLocus = (locus: any) => {
310
384
 
311
385
  return isLocusAsBreakout && !inActiveStatus && selfJoined;
312
386
  };
387
+ /**
388
+ * check if the breakout locus is associated with the main locus by comparing the breakout control url or the replaces info in self device
389
+ * @param {Object} mainLocus main locus data
390
+ * @param {Object} breakoutLocus breakout locus data
391
+ * @returns {boolean}
392
+ * @private
393
+ */
394
+ MeetingsUtil.isMainAssociatedWithBreakout = (mainLocus: any, breakoutLocus: any) => {
395
+ if (
396
+ mainLocus.controls?.breakout?.url &&
397
+ mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
398
+ ) {
399
+ return true;
400
+ }
401
+ const deviceUrl = breakoutLocus?.self?.deviceUrl;
402
+ const replaceInfo = MeetingsUtil.getThisDevice(breakoutLocus, deviceUrl)?.replaces?.[0];
403
+ if (replaceInfo?.locusUrl && replaceInfo.locusUrl === mainLocus.url) {
404
+ return true;
405
+ }
406
+
407
+ return false;
408
+ };
313
409
  export default MeetingsUtil;
@@ -27,6 +27,7 @@ export default class Member {
27
27
  isModerator: any;
28
28
  isModeratorAssignmentProhibited: any;
29
29
  isPresenterAssignmentProhibited: any;
30
+ isAttendeeAssignmentProhibited: any;
30
31
  isMutable: any;
31
32
  isNotAdmitted: any;
32
33
  isRecording: any;
@@ -292,6 +293,14 @@ export default class Member {
292
293
  */
293
294
  this.isPresenterAssignmentProhibited = null;
294
295
 
296
+ /**
297
+ * @instance
298
+ * @type {Boolean}
299
+ * @public
300
+ * @memberof Member
301
+ */
302
+ this.isAttendeeAssignmentProhibited = null;
303
+
295
304
  /**
296
305
  * @instance
297
306
  * @type {Boolean}
@@ -369,6 +378,7 @@ export default class Member {
369
378
  MemberUtil.isModeratorAssignmentProhibited(participant);
370
379
  this.isPresenterAssignmentProhibited =
371
380
  MemberUtil.isPresenterAssignmentProhibited(participant);
381
+ this.isAttendeeAssignmentProhibited = MemberUtil.isAttendeeAssignmentProhibited(participant);
372
382
  this.canApproveAIEnablement = MemberUtil.canApproveAIEnablement(participant);
373
383
  this.processStatus(participant);
374
384
  this.processRoles(participant);
@@ -103,6 +103,7 @@ export interface Participant {
103
103
  moderator: boolean; // Locus docs say this is deprecated and role control should be used instead
104
104
  moderatorAssignmentNotAllowed: boolean;
105
105
  presenterAssignmentNotAllowed: boolean;
106
+ attendeeAssignmentNotAllowed?: boolean;
106
107
  person: ParticipantPerson;
107
108
  resourceGuest: boolean;
108
109
  state: string; // probably one of MEETING_STATE.STATES
@@ -140,6 +140,9 @@ const MemberUtil = {
140
140
  isPresenterAssignmentProhibited: (participant: Participant) =>
141
141
  participant && participant.presenterAssignmentNotAllowed,
142
142
 
143
+ isAttendeeAssignmentProhibited: (participant: Participant) =>
144
+ !!(participant && participant.attendeeAssignmentNotAllowed),
145
+
143
146
  /**
144
147
  * checks to see if the participant id is the same as the passed id
145
148
  * there are multiple ids that can be used
@@ -96,6 +96,9 @@ const BEHAVIORAL_METRICS = {
96
96
  SET_CUSTOM_CODEC_PARAMETERS_USED: 'js_sdk_set_custom_codec_parameters_used',
97
97
  MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED:
98
98
  'js_sdk_mark_custom_codec_parameters_for_deletion_used',
99
+ HASH_TREE_SYNC_FAILURE: 'js_sdk_hash_tree_sync_failure',
100
+ HASH_TREE_HEARTBEAT_WATCHDOG_EXPIRED: 'js_sdk_hash_tree_heartbeat_watchdog_expired',
101
+ HASH_TREE_EMPTY_LOCUS_STATE_ELEMENTS: 'js_sdk_hash_tree_empty_locus_state_elements',
99
102
  };
100
103
 
101
104
  export {BEHAVIORAL_METRICS as default};