From eddcf83464085ba957f9532ffe61d781f779ae0f Mon Sep 17 00:00:00 2001 From: mickelr Date: Thu, 31 Oct 2024 14:17:13 +0800 Subject: [PATCH 01/16] feat(webinar): set panelist or attendee according to roles DTO --- .../plugin-meetings/src/meeting/index.ts | 2 +- .../plugin-meetings/src/webinar/index.ts | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 492981c47ac..963f177dc41 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -3298,7 +3298,7 @@ export default class Meeting extends StatelessWebexPlugin { this.simultaneousInterpretation.updateCanManageInterpreters( payload.newRoles?.includes(SELF_ROLES.MODERATOR) ); - this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR)); + this.webinar.updateRoleChanged(payload); Trigger.trigger( this, { diff --git a/packages/@webex/plugin-meetings/src/webinar/index.ts b/packages/@webex/plugin-meetings/src/webinar/index.ts index aa2016541f6..96b3b8703f9 100644 --- a/packages/@webex/plugin-meetings/src/webinar/index.ts +++ b/packages/@webex/plugin-meetings/src/webinar/index.ts @@ -2,7 +2,8 @@ * Copyright (c) 2015-2023 Cisco Systems, Inc. See LICENSE file. */ import {WebexPlugin} from '@webex/webex-core'; -import {MEETINGS} from '../constants'; +import {get} from 'lodash'; +import {MEETINGS, SELF_ROLES} from '../constants'; import WebinarCollection from './collection'; @@ -20,6 +21,8 @@ const Webinar = WebexPlugin.extend({ webcastUrl: 'string', // current webinar's webcast url webinarAttendeesSearchingUrl: 'string', // current webinarAttendeesSearching url canManageWebcast: 'boolean', // appears the ability to manage webcast + selfIsPanelist: 'boolean', // self is panelist + selfIsAttendee: 'boolean', // self is attendee }, /** @@ -57,6 +60,23 @@ const Webinar = WebexPlugin.extend({ updateCanManageWebcast(canManageWebcast) { this.set('canManageWebcast', canManageWebcast); }, + + /** + * Update whether self has capability to manage start/stop webcast (only host can manage it) + * @param {object} payload + * @returns {void} + */ + updateRoleChanged(payload) { + const isPromoted = + get(payload, 'oldRoles', []).includes(SELF_ROLES.ATTENDEE) && + get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST); + const isDemoted = + get(payload, 'oldRoles', []).includes(SELF_ROLES.PANELIST) && + get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE); + this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST)); + this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE)); + this.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR)); + }, }); export default Webinar; From c8c80c796b58727b0950991268ac76ae282995fd Mon Sep 17 00:00:00 2001 From: mickelr Date: Thu, 31 Oct 2024 15:42:42 +0800 Subject: [PATCH 02/16] feat(webinar): add roles in set mute all control --- .../src/controls-options-manager/enums.ts | 1 + .../src/controls-options-manager/index.ts | 15 +++++++++++++-- .../@webex/plugin-meetings/src/meeting/index.ts | 7 +++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/enums.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/enums.ts index 2e3ac29d86e..323a7ffd1a0 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/enums.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/enums.ts @@ -2,6 +2,7 @@ enum Setting { disallowUnmute = 'DisallowUnmute', muteOnEntry = 'MuteOnEntry', muted = 'Muted', + roles = 'Roles', } enum Control { diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts index d09a2f7446e..9af49d3025f 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts @@ -177,7 +177,7 @@ export default class ControlsOptionsManager { * @memberof ControlsOptionsManager * @returns {Promise} */ - private setControls(setting: {[key in Setting]?: boolean}): Promise { + private setControls(setting: {[key in Setting]?: any}): Promise { LoggerProxy.logger.log( `ControlsOptionsManager:index#setControls --> ${JSON.stringify(setting)}` ); @@ -219,6 +219,14 @@ export default class ControlsOptionsManager { } break; + case Setting.roles: + if (Array.isArray(value)) { + body.audio = body.audio + ? {...body.audio, [camelCase(key)]: value} + : {[camelCase(key)]: value}; + } + break; + default: error = new PermissionError(`${key} [${value}] not allowed, due to moderator property.`); } @@ -261,18 +269,21 @@ export default class ControlsOptionsManager { * @param {boolean} mutedEnabled * @param {boolean} disallowUnmuteEnabled * @param {boolean} muteOnEntryEnabled + * @param {array} roles which should be muted * @memberof ControlsOptionsManager * @returns {Promise} */ public setMuteAll( mutedEnabled: boolean, disallowUnmuteEnabled: boolean, - muteOnEntryEnabled: boolean + muteOnEntryEnabled: boolean, + roles: Array ): Promise { return this.setControls({ [Setting.muted]: mutedEnabled, [Setting.disallowUnmute]: disallowUnmuteEnabled, [Setting.muteOnEntry]: muteOnEntryEnabled, + [Setting.roles]: roles, }); } } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 963f177dc41..b30bf9dc50a 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -7912,18 +7912,21 @@ export default class Meeting extends StatelessWebexPlugin { * @param {boolean} mutedEnabled * @param {boolean} disallowUnmuteEnabled * @param {boolean} muteOnEntryEnabled + * @param {array} roles * @public * @memberof Meeting */ public setMuteAll( mutedEnabled: boolean, disallowUnmuteEnabled: boolean, - muteOnEntryEnabled: boolean + muteOnEntryEnabled: boolean, + roles: Array ) { return this.controlsOptionsManager.setMuteAll( mutedEnabled, disallowUnmuteEnabled, - muteOnEntryEnabled + muteOnEntryEnabled, + roles ); } From 8c807dd5b003ac3093074dcadf1378f549bbd8e7 Mon Sep 17 00:00:00 2001 From: mickelr Date: Fri, 1 Nov 2024 17:32:34 +0800 Subject: [PATCH 03/16] feat(webinar): view participants list panle control update --- .../src/controls-options-manager/types.ts | 2 ++ .../plugin-meetings/src/locus-info/controlsUtils.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts index cefa0a76ddd..dd2b668bb04 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts @@ -36,6 +36,8 @@ export interface VideoProperties { export interface ViewTheParticipantListProperties { enabled?: boolean; + panelistEnabled?: true; + attendeeCount?: false; } export type Properties = diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index f2b4594a74a..495da57b262 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -79,7 +79,11 @@ ControlsUtils.parse = (controls: any) => { } if (controls?.viewTheParticipantList) { - parsedControls.viewTheParticipantList = {enabled: controls.viewTheParticipantList.enabled}; + parsedControls.viewTheParticipantList = { + enabled: controls.viewTheParticipantList.enabled, + panelistEnabled: controls.viewTheParticipantList.panelistEnabled, + attendeeCount: controls.viewTheParticipantList.attendeeCount, + }; } if (controls?.raiseHand) { @@ -121,7 +125,11 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => { previous?.reactions?.showDisplayNameWithReactions, hasViewTheParticipantListChanged: - current?.viewTheParticipantList?.enabled !== previous?.viewTheParticipantList?.enabled, + current?.viewTheParticipantList?.enabled !== previous?.viewTheParticipantList?.enabled || + current?.viewTheParticipantList?.panelistEnabled !== + previous?.viewTheParticipantList?.panelistEnabled || + current?.viewTheParticipantList?.attendeeCount !== + previous?.viewTheParticipantList?.attendeeCount, hasRaiseHandChanged: current?.raiseHand?.enabled !== previous?.raiseHand?.enabled, From ad2df0412b1e33369a3e42b72e356618f584a85d Mon Sep 17 00:00:00 2001 From: mickelr Date: Mon, 4 Nov 2024 16:54:17 +0800 Subject: [PATCH 04/16] feat(webinar): get webcast instance url from DTO --- .../@webex/plugin-meetings/src/constants.ts | 1 + .../src/locus-info/fullState.ts | 1 + .../plugin-meetings/src/locus-info/index.ts | 24 +++++++++++++++++++ .../plugin-meetings/src/meeting/index.ts | 20 ++++++++++++---- .../plugin-meetings/src/webinar/index.ts | 22 +++++------------ 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 424e831a597..3309a97d1bb 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -697,6 +697,7 @@ export const LOCUSINFO = { SELF_MEETING_INTERPRETATION_CHANGED: 'SELF_MEETING_INTERPRETATION_CHANGED', MEDIA_INACTIVITY: 'MEDIA_INACTIVITY', LINKS_SERVICES: 'LINKS_SERVICES', + LINKS_RESOURCES: 'LINKS_RESOURCES', }, }; diff --git a/packages/@webex/plugin-meetings/src/locus-info/fullState.ts b/packages/@webex/plugin-meetings/src/locus-info/fullState.ts index b3feb424da4..4ff7036443e 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/fullState.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/fullState.ts @@ -6,6 +6,7 @@ FullState.parse = (fullState) => ({ type: fullState.type || FULL_STATE.UNKNOWN, meetingState: fullState.state, locked: fullState.locked, + attendeeCount: fullState.attendeeCount, }); FullState.getFullState = (oldFullState, newFullState) => { diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts index 489d12fda57..d01fc0dd118 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/index.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts @@ -64,6 +64,7 @@ export default class LocusInfo extends EventsScope { replace: any; url: any; services: any; + resources: any; mainSessionLocusCache: any; /** * Constructor @@ -263,6 +264,7 @@ export default class LocusInfo extends EventsScope { this.updateHostInfo(locus.host); this.updateMediaShares(locus.mediaShares); this.updateServices(locus.links?.services); + this.updateResources(locus.links?.resources); } /** @@ -452,6 +454,7 @@ export default class LocusInfo extends EventsScope { this.updateIdentifiers(locus.identities); this.updateEmbeddedApps(locus.embeddedApps); this.updateServices(locus.links?.services); + this.updateResources(locus.links?.resources); this.compareAndUpdate(); // update which required to compare different objects from locus } @@ -1064,6 +1067,27 @@ export default class LocusInfo extends EventsScope { } } + /** + * @param {Object} resources + * @returns {undefined} + * @memberof LocusInfo + */ + updateResources(resources: Record<'webcastInstance', {url: string}>) { + if (resources && !isEqual(this.resources, resources)) { + this.resources = resources; + this.emitScoped( + { + file: 'locus-info', + function: 'updateResources', + }, + LOCUSINFO.EVENTS.LINKS_RESOURCES, + { + resources, + } + ); + } + } + /** * @param {Object} fullState * @returns {undefined} diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index b30bf9dc50a..c3caab0fb86 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -2004,6 +2004,7 @@ export default class Meeting extends StatelessWebexPlugin { this.setUpLocusInfoSelfListener(); this.setUpLocusInfoMeetingListener(); this.setUpLocusServicesListener(); + this.setUpLocusResourcesListener(); // members update listeners this.setUpLocusFullStateListener(); this.setUpLocusUrlListener(); @@ -2983,10 +2984,21 @@ export default class Meeting extends StatelessWebexPlugin { this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url); this.annotation.approvalUrlUpdate(payload?.services?.approval?.url); this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url); - this.webinar.webcastUrlUpdate(payload?.services?.webcast?.url); - this.webinar.webinarAttendeesSearchingUrlUpdate( - payload?.services?.webinarAttendeesSearching?.url - ); + }); + } + + /** + * Set up the locus info resources link listener + * update the locusInfo for webcast instance url + * does not currently re-emit the event as it's internal only + * payload is unused + * @returns {undefined} + * @private + * @memberof Meeting + */ + private setUpLocusResourcesListener() { + this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => { + this.webinar.updateWebcastUrl(payload); }); } diff --git a/packages/@webex/plugin-meetings/src/webinar/index.ts b/packages/@webex/plugin-meetings/src/webinar/index.ts index 96b3b8703f9..cc5bad99b9d 100644 --- a/packages/@webex/plugin-meetings/src/webinar/index.ts +++ b/packages/@webex/plugin-meetings/src/webinar/index.ts @@ -18,8 +18,7 @@ const Webinar = WebexPlugin.extend({ props: { locusUrl: 'string', // appears current webinar's locus url - webcastUrl: 'string', // current webinar's webcast url - webinarAttendeesSearchingUrl: 'string', // current webinarAttendeesSearching url + webcastInstanceUrl: 'string', // current webinar's webcast instance url canManageWebcast: 'boolean', // appears the ability to manage webcast selfIsPanelist: 'boolean', // self is panelist selfIsAttendee: 'boolean', // self is attendee @@ -27,7 +26,7 @@ const Webinar = WebexPlugin.extend({ /** * Update the current locus url of the webinar - * @param {string} locusUrl // locus url + * @param {string} locusUrl * @returns {void} */ locusUrlUpdate(locusUrl) { @@ -35,21 +34,12 @@ const Webinar = WebexPlugin.extend({ }, /** - * Update the current webcast url of the meeting - * @param {string} webcastUrl // webcast url - * @returns {void} - */ - webcastUrlUpdate(webcastUrl) { - this.set('webcastUrl', webcastUrl); - }, - - /** - * Update the current webinarAttendeesSearching url of the meeting - * @param {string} webinarAttendeesSearchingUrl // webinarAttendeesSearching url + * Update the current webcast instance url of the meeting + * @param {object} payload * @returns {void} */ - webinarAttendeesSearchingUrlUpdate(webinarAttendeesSearchingUrl) { - this.set('webinarAttendeesSearchingUrl', webinarAttendeesSearchingUrl); + updateWebcastUrl(payload) { + this.set('webcastInstanceUrl', get(payload, 'resources.webcastInstance.url')); }, /** From 46831bc60aa9731d24887d305c49d5bda440a89c Mon Sep 17 00:00:00 2001 From: mickelr Date: Mon, 4 Nov 2024 17:38:07 +0800 Subject: [PATCH 05/16] feat(webinar): handle webcast control from DTO --- packages/@webex/plugin-meetings/src/constants.ts | 2 ++ .../plugin-meetings/src/locus-info/controlsUtils.ts | 9 +++++++++ packages/@webex/plugin-meetings/src/locus-info/index.ts | 9 +++++++++ packages/@webex/plugin-meetings/src/meeting/index.ts | 9 +++++++++ 4 files changed, 29 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 3309a97d1bb..955dbfee8ca 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -356,6 +356,7 @@ export const EVENT_TRIGGERS = { 'meeting:controls:view-the-participants-list:updated', MEETING_CONTROLS_RAISE_HAND_UPDATED: 'meeting:controls:raise-hand:updated', MEETING_CONTROLS_VIDEO_UPDATED: 'meeting:controls:video:updated', + MEETING_CONTROLS_WEBCAST_UPDATED: 'meeting:controls:webcast:updated', // Locus URL changed MEETING_LOCUS_URL_UPDATE: 'meeting:locus:locusUrl:update', MEETING_STREAM_PUBLISH_STATE_CHANGED: 'meeting:streamPublishStateChanged', @@ -671,6 +672,7 @@ export const LOCUSINFO = { CONTROLS_REACTIONS_CHANGED: 'CONTROLS_REACTIONS_CHANGED', CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED: 'CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED', CONTROLS_RAISE_HAND_CHANGED: 'CONTROLS_RAISE_HAND_CHANGED', + CONTROLS_WEBCAST_CHANGED: 'CONTROLS_WEBCAST_CHANGED', CONTROLS_VIDEO_CHANGED: 'CONTROLS_VIDEO_CHANGED', SELF_UNADMITTED_GUEST: 'SELF_UNADMITTED_GUEST', SELF_ADMITTED_GUEST: 'SELF_ADMITTED_GUEST', diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index 495da57b262..98ce9a67c0c 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -94,6 +94,10 @@ ControlsUtils.parse = (controls: any) => { parsedControls.video = {enabled: controls.video.enabled}; } + if (controls?.webcastControl) { + parsedControls.webcastControl = {streaming: controls.webcastControl.streaming}; + } + return parsedControls; }; @@ -175,6 +179,11 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => { hasVideoEnabledChanged: newControls.video?.enabled !== undefined && !isEqual(previous?.videoEnabled, current?.videoEnabled), + + hasWebcastChanged: !isEqual( + previous?.webcastControl.streaming, + current?.webcastControl.streaming + ), }, }; }; diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts index d01fc0dd118..fb5c87698f0 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/index.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts @@ -808,6 +808,7 @@ export default class LocusInfo extends EventsScope { hasRaiseHandChanged, hasVideoChanged, hasInterpretationChanged, + hasWebcastChanged, }, current, } = ControlsUtils.getControls(this.controls, controls); @@ -1011,6 +1012,14 @@ export default class LocusInfo extends EventsScope { ); } + if (hasWebcastChanged) { + this.emitScoped( + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, + {state: current.webcastControl} + ); + } + this.controls = controls; } } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index c3caab0fb86..3711e3713c5 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -2626,6 +2626,15 @@ export default class Meeting extends StatelessWebexPlugin { ); }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, ({state}) => { + Trigger.trigger( + this, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_WEBCAST_UPDATED, + {state} + ); + }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => { Trigger.trigger( this, From 3c6b54f6ec8d78653b567a82daf55f9f7700d1ef Mon Sep 17 00:00:00 2001 From: mickelr Date: Tue, 5 Nov 2024 14:56:56 +0800 Subject: [PATCH 06/16] feat(webinar): new webcast display hint --- packages/@webex/plugin-meetings/src/constants.ts | 4 ++++ .../plugin-meetings/src/locus-info/controlsUtils.ts | 4 ++-- .../plugin-meetings/src/meeting/in-meeting-actions.ts | 8 ++++++++ packages/@webex/plugin-meetings/src/meeting/index.ts | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 955dbfee8ca..704ef6fa875 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -961,6 +961,10 @@ export const DISPLAY_HINTS = { // Voip (audio/video) VOIP_IS_ENABLED: 'VOIP_IS_ENABLED', + + // Webcast + WEBCAST_CONTROL_START: 'WEBCAST_CONTROL_START', + WEBCAST_CONTROL_STOP: 'WEBCAST_CONTROL_STOP', }; export const INTERSTITIAL_DISPLAY_HINTS = [DISPLAY_HINTS.VOIP_IS_ENABLED]; diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index 98ce9a67c0c..9a6e36ab383 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -181,8 +181,8 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => { !isEqual(previous?.videoEnabled, current?.videoEnabled), hasWebcastChanged: !isEqual( - previous?.webcastControl.streaming, - current?.webcastControl.streaming + previous?.webcastControl?.streaming, + current?.webcastControl?.streaming ), }, }; diff --git a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts index 161e90a9fbe..28606cd4a4c 100644 --- a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts @@ -83,6 +83,8 @@ interface IInMeetingActions { canShareWhiteBoard?: boolean; enforceVirtualBackground?: boolean; canPollingAndQA?: boolean; + canStartWebcast?: boolean; + canStopWebcast?: boolean; } /** @@ -238,6 +240,10 @@ export default class InMeetingActions implements IInMeetingActions { canShareWhiteBoard = null; canPollingAndQA = null; + + canStartWebcast = null; + + canStopWebcast = null; /** * Returns all meeting action options * @returns {Object} @@ -317,6 +323,8 @@ export default class InMeetingActions implements IInMeetingActions { supportHDV: this.supportHDV, canShareWhiteBoard: this.canShareWhiteBoard, canPollingAndQA: this.canPollingAndQA, + canStartWebcast: this.canStartWebcast, + canStopWebcast: this.canStopWebcast, }); /** diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 3711e3713c5..1b2e821e6f5 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -3829,6 +3829,14 @@ export default class Meeting extends StatelessWebexPlugin { requiredHints: [DISPLAY_HINTS.DISABLE_VIDEO], displayHints: this.userDisplayHints, }), + canStartWebcast: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.WEBCAST_CONTROL_START], + displayHints: this.userDisplayHints, + }), + canStopWebcast: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.WEBCAST_CONTROL_STOP], + displayHints: this.userDisplayHints, + }), canShareFile: (ControlsOptionsUtil.hasHints({ requiredHints: [DISPLAY_HINTS.SHARE_FILE], From b9f5a5da48983a189a8ffdd2539e87edf9e0d052 Mon Sep 17 00:00:00 2001 From: mickelr Date: Tue, 5 Nov 2024 16:25:36 +0800 Subject: [PATCH 07/16] feat(webinar): handle meeting full control from DTO --- packages/@webex/plugin-meetings/src/constants.ts | 2 ++ .../src/locus-info/controlsUtils.ts | 14 ++++++++++++++ .../@webex/plugin-meetings/src/locus-info/index.ts | 9 +++++++++ .../@webex/plugin-meetings/src/meeting/index.ts | 9 +++++++++ 4 files changed, 34 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 704ef6fa875..891abd24eeb 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -357,6 +357,7 @@ export const EVENT_TRIGGERS = { MEETING_CONTROLS_RAISE_HAND_UPDATED: 'meeting:controls:raise-hand:updated', MEETING_CONTROLS_VIDEO_UPDATED: 'meeting:controls:video:updated', MEETING_CONTROLS_WEBCAST_UPDATED: 'meeting:controls:webcast:updated', + MEETING_CONTROLS_MEETING_FULL_UPDATED: 'meeting:controls:meeting-full:updated', // Locus URL changed MEETING_LOCUS_URL_UPDATE: 'meeting:locus:locusUrl:update', MEETING_STREAM_PUBLISH_STATE_CHANGED: 'meeting:streamPublishStateChanged', @@ -673,6 +674,7 @@ export const LOCUSINFO = { CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED: 'CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED', CONTROLS_RAISE_HAND_CHANGED: 'CONTROLS_RAISE_HAND_CHANGED', CONTROLS_WEBCAST_CHANGED: 'CONTROLS_WEBCAST_CHANGED', + CONTROLS_MEETING_FULL_CHANGED: 'CONTROLS_MEETING_FULL_CHANGED', CONTROLS_VIDEO_CHANGED: 'CONTROLS_VIDEO_CHANGED', SELF_UNADMITTED_GUEST: 'SELF_UNADMITTED_GUEST', SELF_ADMITTED_GUEST: 'SELF_ADMITTED_GUEST', diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index 9a6e36ab383..09fb8b9a57e 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -98,6 +98,13 @@ ControlsUtils.parse = (controls: any) => { parsedControls.webcastControl = {streaming: controls.webcastControl.streaming}; } + if (controls?.meetingFull) { + parsedControls.meetingFull = { + meetingFull: controls.meetingFull.meetingFull, + meetingPanelistFull: controls.meetingFull.meetingPanelistFull, + }; + } + return parsedControls; }; @@ -184,6 +191,13 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => { previous?.webcastControl?.streaming, current?.webcastControl?.streaming ), + + hasMeetingFullChanged: + !isEqual(previous?.meetingFull?.meetingFull, current?.meetingFull?.meetingFull) || + !isEqual( + previous?.meetingFull?.meetingPanelistFull, + current?.meetingFull?.meetingPanelistFull + ), }, }; }; diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts index fb5c87698f0..e606ba3f144 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/index.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts @@ -809,6 +809,7 @@ export default class LocusInfo extends EventsScope { hasVideoChanged, hasInterpretationChanged, hasWebcastChanged, + hasMeetingFullChanged, }, current, } = ControlsUtils.getControls(this.controls, controls); @@ -1020,6 +1021,14 @@ export default class LocusInfo extends EventsScope { ); } + if (hasMeetingFullChanged) { + this.emitScoped( + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_MEETING_FULL_CHANGED, + {state: current.meetingFull} + ); + } + this.controls = controls; } } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 1b2e821e6f5..1264d5ef428 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -2635,6 +2635,15 @@ export default class Meeting extends StatelessWebexPlugin { ); }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_FULL_CHANGED, ({state}) => { + Trigger.trigger( + this, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_MEETING_FULL_UPDATED, + {state} + ); + }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => { Trigger.trigger( this, From 9c48a68f7351149deebf792bdb93c0d9e5506474 Mon Sep 17 00:00:00 2001 From: mickelr Date: Fri, 8 Nov 2024 09:52:01 +0800 Subject: [PATCH 08/16] feat(webinar): handle practice session control from DTO --- packages/@webex/plugin-meetings/src/constants.ts | 3 +++ .../plugin-meetings/src/locus-info/controlsUtils.ts | 11 +++++++++++ .../@webex/plugin-meetings/src/locus-info/index.ts | 9 +++++++++ packages/@webex/plugin-meetings/src/meeting/index.ts | 9 +++++++++ 4 files changed, 32 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 891abd24eeb..4c5b182da71 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -358,6 +358,8 @@ export const EVENT_TRIGGERS = { MEETING_CONTROLS_VIDEO_UPDATED: 'meeting:controls:video:updated', MEETING_CONTROLS_WEBCAST_UPDATED: 'meeting:controls:webcast:updated', MEETING_CONTROLS_MEETING_FULL_UPDATED: 'meeting:controls:meeting-full:updated', + MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED: + 'meeting:controls:practice-session-status:updated', // Locus URL changed MEETING_LOCUS_URL_UPDATE: 'meeting:locus:locusUrl:update', MEETING_STREAM_PUBLISH_STATE_CHANGED: 'meeting:streamPublishStateChanged', @@ -675,6 +677,7 @@ export const LOCUSINFO = { CONTROLS_RAISE_HAND_CHANGED: 'CONTROLS_RAISE_HAND_CHANGED', CONTROLS_WEBCAST_CHANGED: 'CONTROLS_WEBCAST_CHANGED', CONTROLS_MEETING_FULL_CHANGED: 'CONTROLS_MEETING_FULL_CHANGED', + CONTROLS_PRACTICE_SESSION_STATUS_UPDATED: 'CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', CONTROLS_VIDEO_CHANGED: 'CONTROLS_VIDEO_CHANGED', SELF_UNADMITTED_GUEST: 'SELF_UNADMITTED_GUEST', SELF_ADMITTED_GUEST: 'SELF_ADMITTED_GUEST', diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index 09fb8b9a57e..7bc0296d6df 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -105,6 +105,12 @@ ControlsUtils.parse = (controls: any) => { }; } + if (controls?.practiceSession) { + parsedControls.practiceSession = { + enabled: controls.practiceSession.enabled, + }; + } + return parsedControls; }; @@ -198,6 +204,11 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => { previous?.meetingFull?.meetingPanelistFull, current?.meetingFull?.meetingPanelistFull ), + + hasPracticeSessionEnabledChanged: !isEqual( + previous?.practiceSession?.enabled, + current?.practiceSession?.enabled + ), }, }; }; diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts index e606ba3f144..cba9538fe1f 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/index.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts @@ -810,6 +810,7 @@ export default class LocusInfo extends EventsScope { hasInterpretationChanged, hasWebcastChanged, hasMeetingFullChanged, + hasPracticeSessionEnabledChanged, }, current, } = ControlsUtils.getControls(this.controls, controls); @@ -1029,6 +1030,14 @@ export default class LocusInfo extends EventsScope { ); } + if (hasPracticeSessionEnabledChanged) { + this.emitScoped( + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, + {state: current.practiceSession} + ); + } + this.controls = controls; } } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index f62815d915c..08c205db22f 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -2644,6 +2644,15 @@ export default class Meeting extends StatelessWebexPlugin { ); }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => { + Trigger.trigger( + this, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, + {state} + ); + }); + this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => { Trigger.trigger( this, From 93c4121a30c0afc30b2f06e5f4a7e80adc86033a Mon Sep 17 00:00:00 2001 From: mickelr Date: Fri, 8 Nov 2024 17:03:42 +0800 Subject: [PATCH 09/16] feat(webinar): new display hints for plist control in webinar --- packages/@webex/plugin-meetings/src/constants.ts | 5 +++++ .../src/controls-options-manager/util.ts | 14 ++++++++++++++ .../src/meeting/in-meeting-actions.ts | 16 ++++++++++++++++ .../@webex/plugin-meetings/src/meeting/index.ts | 16 ++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 4c5b182da71..6dab5f089ae 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -947,6 +947,11 @@ export const DISPLAY_HINTS = { // participants list DISABLE_VIEW_THE_PARTICIPANT_LIST: 'DISABLE_VIEW_THE_PARTICIPANT_LIST', ENABLE_VIEW_THE_PARTICIPANT_LIST: 'ENABLE_VIEW_THE_PARTICIPANT_LIST', + // for webinar participants list + DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST: 'DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST', + ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST: 'ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST', + DISABLE_SHOW_ATTENDEE_COUNT: 'DISABLE_SHOW_ATTENDEE_COUNT', + ENABLE_SHOW_ATTENDEE_COUNT: 'ENABLE_SHOW_ATTENDEE_COUNT', // raise hand DISABLE_RAISE_HAND: 'DISABLE_RAISE_HAND', diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts index a297e51b405..b8340a9eb04 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts @@ -217,6 +217,20 @@ class Utils { if (control.properties.enabled === false) { requiredHints.push(DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST); } + if (control.properties.panelistEnabled === true) { + requiredHints.push(DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST); + } + // @ts-ignore + if (control.properties.panelistEnabled === false) { + requiredHints.push(DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST); + } + // @ts-ignore + if (control.properties.attendeeCount === true) { + requiredHints.push(DISPLAY_HINTS.ENABLE_SHOW_ATTENDEE_COUNT); + } + if (control.properties.attendeeCount === false) { + requiredHints.push(DISPLAY_HINTS.DISABLE_SHOW_ATTENDEE_COUNT); + } return Utils.hasHints({requiredHints, displayHints}); } diff --git a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts index 28606cd4a4c..d4a5902c38c 100644 --- a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts @@ -64,6 +64,10 @@ interface IInMeetingActions { canUpdateShareControl?: boolean; canEnableViewTheParticipantsList?: boolean; canDisableViewTheParticipantsList?: boolean; + canEnableViewTheParticipantsListPanelist?: boolean; + canDisableViewTheParticipantsListPanelist?: boolean; + canEnableShowAttendeeCount?: boolean; + canDisableShowAttendeeCount?: boolean; canEnableRaiseHand?: boolean; canDisableRaiseHand?: boolean; canEnableVideo?: boolean; @@ -203,6 +207,14 @@ export default class InMeetingActions implements IInMeetingActions { canDisableViewTheParticipantsList = null; + canEnableViewTheParticipantsListPanelist = null; + + canDisableViewTheParticipantsListPanelist = null; + + canEnableShowAttendeeCount = null; + + canDisableShowAttendeeCount = null; + canEnableRaiseHand = null; canDisableRaiseHand = null; @@ -304,6 +316,10 @@ export default class InMeetingActions implements IInMeetingActions { canUpdateShareControl: this.canUpdateShareControl, canEnableViewTheParticipantsList: this.canEnableViewTheParticipantsList, canDisableViewTheParticipantsList: this.canDisableViewTheParticipantsList, + canEnableViewTheParticipantsListPanelist: this.canEnableViewTheParticipantsListPanelist, + canDisableViewTheParticipantsListPanelist: this.canDisableViewTheParticipantsListPanelist, + canEnableShowAttendeeCount: this.canEnableShowAttendeeCount, + canDisableShowAttendeeCount: this.canDisableShowAttendeeCount, canEnableRaiseHand: this.canEnableRaiseHand, canDisableRaiseHand: this.canDisableRaiseHand, canEnableVideo: this.canEnableVideo, diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 08c205db22f..c04f380c658 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -3831,6 +3831,22 @@ export default class Meeting extends StatelessWebexPlugin { requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST], displayHints: this.userDisplayHints, }), + canEnableViewTheParticipantsListPanelist: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST], + displayHints: this.userDisplayHints, + }), + canDisableViewTheParticipantsListPanelist: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST], + displayHints: this.userDisplayHints, + }), + canEnableShowAttendeeCount: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_ATTENDEE_COUNT], + displayHints: this.userDisplayHints, + }), + canDisableShowAttendeeCount: ControlsOptionsUtil.hasHints({ + requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_ATTENDEE_COUNT], + displayHints: this.userDisplayHints, + }), canEnableRaiseHand: ControlsOptionsUtil.hasHints({ requiredHints: [DISPLAY_HINTS.ENABLE_RAISE_HAND], displayHints: this.userDisplayHints, From b32b5ad1865a6e1663fd0046f7619e1362b84205 Mon Sep 17 00:00:00 2001 From: mickelr Date: Tue, 12 Nov 2024 15:43:32 +0800 Subject: [PATCH 10/16] feat(webinar): add unit test for new code --- .../src/controls-options-manager/index.ts | 1 + .../spec/controls-options-manager/index.js | 84 ++++++++++------ .../spec/controls-options-manager/util.js | 44 +++++++++ .../unit/spec/locus-info/controlsUtils.js | 73 +++++++++++++- .../test/unit/spec/locus-info/index.js | 59 ++++++++++- .../unit/spec/meeting/in-meeting-actions.ts | 12 +++ .../test/unit/spec/meeting/index.js | 98 ++++++++++++++++--- .../test/unit/spec/webinar/index.ts | 15 +-- 8 files changed, 323 insertions(+), 63 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts index 9af49d3025f..73a383fdc42 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts @@ -190,6 +190,7 @@ export default class ControlsOptionsManager { Object.entries(setting).forEach(([key, value]) => { if ( !shouldSkipCheckToMergeBody && + value !== undefined && !Util?.[`${value ? CAN_SET : CAN_UNSET}${key}`](this.displayHints) ) { error = new PermissionError(`${key} [${value}] not allowed, due to moderator property.`); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js index 4b0e84e0be6..00bfe506504 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js @@ -22,7 +22,7 @@ describe('plugin-meetings', () => { describe('Mute On Entry', () => { let manager; - + beforeEach(() => { request = { request: sinon.stub().returns(Promise.resolve()), @@ -37,85 +37,85 @@ describe('plugin-meetings', () => { }); describe('setMuteOnEntry', () => { - it('rejects when correct display hint is not present enabled=false', () => { + it('rejects when correct display hint is not present enabled=false', () => { const result = manager.setMuteOnEntry(false); - + assert.notCalled(request.request); - + assert.isRejected(result); }); - it('rejects when correct display hint is not present enabled=true', () => { + it('rejects when correct display hint is not present enabled=true', () => { const result = manager.setMuteOnEntry(true); - + assert.notCalled(request.request); - + assert.isRejected(result); }); - + it('can set mute on entry when the display hint is available enabled=true', () => { manager.setDisplayHints(['ENABLE_MUTE_ON_ENTRY']); - + const result = manager.setMuteOnEntry(true); - + assert.calledWith(request.request, { uri: 'test/id/controls', body: { muteOnEntry: { enabled: true } }, method: HTTP_VERBS.PATCH}); - + assert.deepEqual(result, request.request.firstCall.returnValue); }); it('can set mute on entry when the display hint is available enabled=false', () => { manager.setDisplayHints(['DISABLE_MUTE_ON_ENTRY']); - + const result = manager.setMuteOnEntry(false); - + assert.calledWith(request.request, { uri: 'test/id/controls', body: { muteOnEntry: { enabled: false } }, method: HTTP_VERBS.PATCH}); - + assert.deepEqual(result, request.request.firstCall.returnValue); }); }); describe('setDisallowUnmute', () => { - it('rejects when correct display hint is not present enabled=false', () => { + it('rejects when correct display hint is not present enabled=false', () => { const result = manager.setDisallowUnmute(false); - + assert.notCalled(request.request); - + assert.isRejected(result); }); - it('rejects when correct display hint is not present enabled=true', () => { + it('rejects when correct display hint is not present enabled=true', () => { const result = manager.setDisallowUnmute(true); - + assert.notCalled(request.request); - + assert.isRejected(result); }); - + it('can set mute on entry when the display hint is available enabled=true', () => { manager.setDisplayHints(['ENABLE_HARD_MUTE']); - + const result = manager.setDisallowUnmute(true); - + assert.calledWith(request.request, { uri: 'test/id/controls', body: { disallowUnmute: { enabled: true } }, method: HTTP_VERBS.PATCH}); - + assert.deepEqual(result, request.request.firstCall.returnValue); }); it('can set mute on entry when the display hint is available enabled=false', () => { manager.setDisplayHints(['DISABLE_HARD_MUTE']); - + const result = manager.setDisallowUnmute(false); - + assert.calledWith(request.request, { uri: 'test/id/controls', body: { disallowUnmute: { enabled: false } }, method: HTTP_VERBS.PATCH}); - + assert.deepEqual(result, request.request.firstCall.returnValue); }); }); @@ -218,7 +218,7 @@ describe('plugin-meetings', () => { }) }); - it('rejects when correct display hint is not present mutedEnabled=false', () => { + it('rejects when correct display hint is not present mutedEnabled=false', () => { const result = manager.setMuteAll(false, false, false); assert.notCalled(request.request); @@ -226,7 +226,7 @@ describe('plugin-meetings', () => { assert.isRejected(result); }); - it('rejects when correct display hint is not present mutedEnabled=true', () => { + it('rejects when correct display hint is not present mutedEnabled=true', () => { const result = manager.setMuteAll(true, false, false); assert.notCalled(request.request); @@ -281,7 +281,31 @@ describe('plugin-meetings', () => { assert.deepEqual(result, request.request.firstCall.returnValue); }); + + it('can set mute all panelists when the display hint is available mutedEnabled=true', () => { + manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']); + + const result = manager.setMuteAll(true, true, true, ['panelist']); + + assert.calledWith(request.request, { uri: 'test/id/controls', + body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['panelist'] } }, + method: HTTP_VERBS.PATCH}); + + assert.deepEqual(result, request.request.firstCall.returnValue); + }); + + it('can set mute all attendees when the display hint is available mutedEnabled=true', () => { + manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']); + + const result = manager.setMuteAll(true, true, true, ['attendee']); + + assert.calledWith(request.request, { uri: 'test/id/controls', + body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } }, + method: HTTP_VERBS.PATCH}); + + assert.deepEqual(result, request.request.firstCall.returnValue); + }); }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/util.js b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/util.js index 375cf02487b..083c4399e4e 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/util.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/util.js @@ -348,6 +348,50 @@ describe('plugin-meetings', () => { }); }); + it('should call hasHints() with proper hints when `panelistEnabled` is true, attendeeCount is false', () => { + ControlsOptionsUtil.canUpdateViewTheParticipantsList({properties: {enabled: true, panelistEnabled: true, attendeeCount: false}}, []); + + assert.calledWith(ControlsOptionsUtil.hasHints, { + requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST, + DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST, + DISPLAY_HINTS.DISABLE_SHOW_ATTENDEE_COUNT], + displayHints: [], + }); + }); + + it('should call hasHints() with proper hints when `panelistEnabled` is true, attendeeCount is true', () => { + ControlsOptionsUtil.canUpdateViewTheParticipantsList({properties: {enabled: true, panelistEnabled: true, attendeeCount: true}}, []); + + assert.calledWith(ControlsOptionsUtil.hasHints, { + requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST, + DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST, + DISPLAY_HINTS.ENABLE_SHOW_ATTENDEE_COUNT], + displayHints: [], + }); + }); + + it('should call hasHints() with proper hints when `panelistEnabled` is false, attendeeCount is false', () => { + ControlsOptionsUtil.canUpdateViewTheParticipantsList({properties: {enabled: true, panelistEnabled: false, attendeeCount: false}}, []); + + assert.calledWith(ControlsOptionsUtil.hasHints, { + requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST, + DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST, + DISPLAY_HINTS.DISABLE_SHOW_ATTENDEE_COUNT], + displayHints: [], + }); + }); + + it('should call hasHints() with proper hints when `panelistEnabled` is false, attendeeCount is true', () => { + ControlsOptionsUtil.canUpdateViewTheParticipantsList({properties: {enabled: true, panelistEnabled: false, attendeeCount: true}}, []); + + assert.calledWith(ControlsOptionsUtil.hasHints, { + requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST, + DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST, + DISPLAY_HINTS.ENABLE_SHOW_ATTENDEE_COUNT], + displayHints: [], + }); + }); + it('should return the resolution of hasHints()', () => { const expected = 'example-return-value'; ControlsOptionsUtil.hasHints.returns(expected); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/controlsUtils.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/controlsUtils.js index 92249d2a4bb..50c79ee922d 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/controlsUtils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/controlsUtils.js @@ -82,11 +82,13 @@ describe('plugin-meetings', () => { }); it('should parse the viewTheParticipantList control', () => { - const newControls = {viewTheParticipantList: {enabled: true}}; + const newControls = {viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: false}}; const parsedControls = ControlsUtils.parse(newControls); assert.equal(parsedControls.viewTheParticipantList.enabled, newControls.viewTheParticipantList.enabled); + assert.equal(parsedControls.viewTheParticipantList.panelistEnabled, newControls.viewTheParticipantList.panelistEnabled); + assert.equal(parsedControls.viewTheParticipantList.attendeeCount, newControls.viewTheParticipantList.attendeeCount); }); it('should parse the raiseHand control', () => { @@ -105,6 +107,31 @@ describe('plugin-meetings', () => { assert.equal(parsedControls.video.enabled, newControls.video.enabled); }); + it('should parse the webcast control', () => { + const newControls = {webcastControl: {streaming: true}}; + + const parsedControls = ControlsUtils.parse(newControls); + + assert.equal(parsedControls.webcastControl.streaming, newControls.webcastControl.streaming); + }); + + it('should parse the meeting full control', () => { + const newControls = {meetingFull: {meetingFull: true, meetingPanelistFull: false}}; + + const parsedControls = ControlsUtils.parse(newControls); + + assert.equal(parsedControls.meetingFull.meetingFull, newControls.meetingFull.meetingFull); + assert.equal(parsedControls.meetingFull.meetingPanelistFull, newControls.meetingFull.meetingPanelistFull); + }); + + it('should parse the practiceSession control', () => { + const newControls = {practiceSession: {enabled: true}}; + + const parsedControls = ControlsUtils.parse(newControls); + + assert.equal(parsedControls.practiceSession.enabled, newControls.practiceSession.enabled); + }); + describe('videoEnabled', () => { it('returns expected', () => { const result = ControlsUtils.parse({video: {enabled: true}}); @@ -170,11 +197,21 @@ describe('plugin-meetings', () => { }); it('returns hasViewTheParticipantListChanged = true when changed', () => { - const newControls = {viewTheParticipantList: {enabled: true}}; + const oldControls = {viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: false}}; - const {updates} = ControlsUtils.getControls(defaultControls, newControls); + let result = ControlsUtils.getControls(oldControls, {viewTheParticipantList: {enabled: false, panelistEnabled: true, attendeeCount: false}}); + + assert.equal(result.updates.hasViewTheParticipantListChanged, true); + + result = ControlsUtils.getControls(oldControls, {viewTheParticipantList: {enabled: true, panelistEnabled: false, attendeeCount: false}}); - assert.equal(updates.hasViewTheParticipantListChanged, true); + assert.equal(result.updates.hasViewTheParticipantListChanged, true); + result = ControlsUtils.getControls(oldControls, {viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: true}}); + + assert.equal(result.updates.hasViewTheParticipantListChanged, true); + result = ControlsUtils.getControls(oldControls, {viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: false}}); + + assert.equal(result.updates.hasViewTheParticipantListChanged, false); }); it('returns hasRaiseHandChanged = true when changed', () => { @@ -193,6 +230,34 @@ describe('plugin-meetings', () => { assert.equal(updates.hasVideoChanged, true); }); + it('returns hasWebcastChanged = true when changed', () => { + const newControls = {webcastControl: {streaming: true}}; + + const {updates} = ControlsUtils.getControls(defaultControls, newControls); + + assert.equal(updates.hasWebcastChanged, true); + }); + + it('returns hasMeetingFullChanged = true when changed', () => { + const newControls = {meetingFull: {meetingFull: true, meetingPanelistFull: false}}; + + let result = ControlsUtils.getControls(defaultControls, newControls); + + assert.equal(result.updates.hasMeetingFullChanged, true); + + result = ControlsUtils.getControls(newControls, {meetingFull: {meetingFull: true, meetingPanelistFull: true}}); + + assert.equal(result.updates.hasMeetingFullChanged, true); + }); + + it('returns hasPracticeSessionEnabledChanged = true when changed', () => { + const newControls = {practiceSession: {enabled: true}}; + + const {updates} = ControlsUtils.getControls(defaultControls, newControls); + + assert.equal(updates.hasPracticeSessionEnabledChanged, true); + }); + it('returns hasEntryExitToneChanged = true when mode changed', () => { const newControls = { entryExitTone: { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js index 359788cccb5..1150e22b553 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js @@ -81,7 +81,7 @@ describe('plugin-meetings', () => { newControls = { disallowUnmute: {enabled: true}, lock: {}, - meetingFull: {}, + meetingFull: {meetingFull: false, meetingPanelistFull: true}, muteOnEntry: {enabled: true}, raiseHand: {enabled: true}, reactions: {enabled: true, showDisplayNameWithReactions: true}, @@ -95,12 +95,14 @@ describe('plugin-meetings', () => { }, shareControl: {control: 'example-value'}, transcribe: {}, - viewTheParticipantList: {enabled: true}, + viewTheParticipantList: {enabled: true, panelistEnabled: true, attendeeCount: false}, meetingContainer: { meetingContainerUrl: 'http://new-url.com', }, entryExitTone: {enabled: true, mode: 'foo'}, video: {enabled: true}, + webcastControl: {streaming: false}, + practiceSession: {enabled: true}, }; }); @@ -204,6 +206,57 @@ describe('plugin-meetings', () => { {state: newControls.video} ); }); + it('should trigger the CONTROLS_VIDEO_CHANGED event when necessary', () => { + locusInfo.controls = {}; + locusInfo.emitScoped = sinon.stub(); + locusInfo.updateControls(newControls); + + assert.calledWith( + locusInfo.emitScoped, + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, + {state: newControls.video} + ); + }); + + it('should trigger the CONTROLS_WEBCAST_CHANGED event when necessary', () => { + locusInfo.controls = {}; + locusInfo.emitScoped = sinon.stub(); + locusInfo.updateControls(newControls); + + assert.calledWith( + locusInfo.emitScoped, + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, + {state: newControls.webcastControl} + ); + }); + + it('should trigger the CONTROLS_MEETING_FULL_CHANGED event when necessary', () => { + locusInfo.controls = {}; + locusInfo.emitScoped = sinon.stub(); + locusInfo.updateControls(newControls); + + assert.calledWith( + locusInfo.emitScoped, + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_MEETING_FULL_CHANGED, + {state: newControls.meetingFull} + ); + }); + + it('should trigger the CONTROLS_PRACTICE_SESSION_STATUS_UPDATED event when necessary', () => { + locusInfo.controls = {}; + locusInfo.emitScoped = sinon.stub(); + locusInfo.updateControls(newControls); + + assert.calledWith( + locusInfo.emitScoped, + {file: 'locus-info', function: 'updateControls'}, + LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, + {state: newControls.practiceSession} + ); + }); it('should not trigger the CONTROLS_RECORDING_UPDATED event', () => { locusInfo.controls = {}; @@ -1729,6 +1782,7 @@ describe('plugin-meetings', () => { locusInfo.updateMemberShip = sinon.stub(); locusInfo.updateIdentifiers = sinon.stub(); locusInfo.updateEmbeddedApps = sinon.stub(); + locusInfo.updateResources = sinon.stub(); locusInfo.compareAndUpdate = sinon.stub(); locusInfo.updateLocusInfo(newLocus); @@ -1750,6 +1804,7 @@ describe('plugin-meetings', () => { assert.notCalled(locusInfo.updateMemberShip); assert.notCalled(locusInfo.updateIdentifiers); assert.notCalled(locusInfo.updateEmbeddedApps); + assert.notCalled(locusInfo.updateResources); assert.notCalled(locusInfo.compareAndUpdate); }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts index 9fe27480635..90784f1664c 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts @@ -60,6 +60,10 @@ describe('plugin-meetings', () => { canUpdateShareControl: null, canEnableViewTheParticipantsList: null, canDisableViewTheParticipantsList: null, + canEnableViewTheParticipantsListPanelist: null, + canDisableViewTheParticipantsListPanelist: null, + canEnableShowAttendeeCount: null, + canDisableShowAttendeeCount: null, canEnableRaiseHand: null, canDisableRaiseHand: null, canEnableVideo: null, @@ -79,6 +83,8 @@ describe('plugin-meetings', () => { canShareWhiteBoard: null, enforceVirtualBackground: null, canPollingAndQA: null, + canStartWebcast: null, + canStopWebcast: null, ...expected, }; @@ -144,6 +150,10 @@ describe('plugin-meetings', () => { 'canUpdateShareControl', 'canEnableViewTheParticipantsList', 'canDisableViewTheParticipantsList', + 'canEnableViewTheParticipantsListPanelist', + 'canDisableViewTheParticipantsListPanelist', + 'canEnableShowAttendeeCount', + 'canDisableShowAttendeeCount', 'canEnableRaiseHand', 'canDisableRaiseHand', 'canEnableVideo', @@ -163,6 +173,8 @@ describe('plugin-meetings', () => { 'canShareWhiteBoard', 'enforceVirtualBackground', 'canPollingAndQA', + 'canStartWebcast', + 'canStopWebcast', ].forEach((key) => { it(`get and set for ${key} work as expected`, () => { const inMeetingActions = new InMeetingActions(); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index bd71da9a7e2..5469fb085e0 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -8863,6 +8863,60 @@ describe('plugin-meetings', () => { ); }); + it('listens to MEETING_CONTROLS_WEBCAST_UPDATED', async () => { + const state = {example: 'value'}; + + await meeting.locusInfo.emitScoped( + {function: 'test', file: 'test'}, + LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, + {state} + ); + + assert.calledWith( + TriggerProxy.trigger, + meeting, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_WEBCAST_UPDATED, + {state} + ); + }); + + it('listens to MEETING_CONTROLS_MEETING_FULL_UPDATED', async () => { + const state = {example: 'value'}; + + await meeting.locusInfo.emitScoped( + {function: 'test', file: 'test'}, + LOCUSINFO.EVENTS.CONTROLS_MEETING_FULL_CHANGED, + {state} + ); + + assert.calledWith( + TriggerProxy.trigger, + meeting, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_MEETING_FULL_UPDATED, + {state} + ); + }); + + it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => { + const state = {example: 'value'}; + + await meeting.locusInfo.emitScoped( + {function: 'test', file: 'test'}, + LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, + {state} + ); + + assert.calledWith( + TriggerProxy.trigger, + meeting, + {file: 'meeting/index', function: 'setupLocusControlsListener'}, + EVENT_TRIGGERS.MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, + {state} + ); + }); + it('listens to MEETING_CONTROLS_VIDEO_UPDATED', async () => { const state = {example: 'value'}; @@ -8976,12 +9030,6 @@ describe('plugin-meetings', () => { approval: { url: 'url', }, - webcast: { - url: 'url', - }, - webinarAttendeesSearching: { - url: 'url', - }, }, }; @@ -8995,10 +9043,6 @@ describe('plugin-meetings', () => { meeting.simultaneousInterpretation = { approvalUrlUpdate: sinon.stub().returns(undefined), }; - meeting.webinar = { - webcastUrlUpdate: sinon.stub().returns(undefined), - webinarAttendeesSearchingUrlUpdate: sinon.stub().returns(undefined), - }; meeting.locusInfo.emit( {function: 'test', file: 'test'}, @@ -9018,15 +9062,37 @@ describe('plugin-meetings', () => { meeting.simultaneousInterpretation.approvalUrlUpdate, newLocusServices.services.approval.url ); - assert.calledWith( - meeting.webinar.webcastUrlUpdate, - newLocusServices.services.webcast.url + assert.calledOnce(meeting.recordingController.setSessionId); + done(); + }); + }); + + describe('#setUpLocusResourcesListener', () => { + it('listens to the locus resources update event', (done) => { + const newLocusResources = { + resources: { + webcastInstance: { + url: 'url', + }, + + }, + }; + + meeting.webinar = { + updateWebcastUrl: sinon.stub().returns(undefined), + }; + + meeting.locusInfo.emit( + {function: 'test', file: 'test'}, + 'LINKS_RESOURCES', + newLocusResources ); + assert.calledWith( - meeting.webinar.webinarAttendeesSearchingUrlUpdate, - newLocusServices.services.webinarAttendeesSearching.url + meeting.webinar.updateWebcastUrl, + newLocusResources ); - assert.calledOnce(meeting.recordingController.setSessionId); + done(); }); }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts index d4988d44bee..9d7a3de4f85 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts @@ -29,21 +29,14 @@ describe('plugin-meetings', () => { }); }); - describe('#webcastUrlUpdate', () => { - it('sets the webcast url', () => { - webinar.webcastUrlUpdate('newUrl'); + describe('#updateWebcastUrl', () => { + it('sets the webcast instance url', () => { + webinar.updateWebcastUrl({resources: {webcastInstance: {url:'newUrl'}}}); - assert.equal(webinar.webcastUrl, 'newUrl'); + assert.equal(webinar.webcastInstanceUrl, 'newUrl'); }); }); - describe('#webinarAttendeesSearchingUrlUpdate', () => { - it('sets the webinarAttendeesSearching url', () => { - webinar.webinarAttendeesSearchingUrlUpdate('newUrl'); - - assert.equal(webinar.webinarAttendeesSearchingUrl, 'newUrl'); - }); - }); describe('#updateCanManageWebcast', () => { it('update canManageWebcast', () => { From fb00d2acdd4f1b2aad87d11122b30ead5a70c69b Mon Sep 17 00:00:00 2001 From: mickelr Date: Wed, 13 Nov 2024 13:39:01 +0800 Subject: [PATCH 11/16] feat(webinar): add unit test for new code --- .../plugin-meetings/src/webinar/index.ts | 2 + .../test/unit/spec/webinar/index.ts | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/webinar/index.ts b/packages/@webex/plugin-meetings/src/webinar/index.ts index cc5bad99b9d..d057ab8fe1b 100644 --- a/packages/@webex/plugin-meetings/src/webinar/index.ts +++ b/packages/@webex/plugin-meetings/src/webinar/index.ts @@ -66,6 +66,8 @@ const Webinar = WebexPlugin.extend({ this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST)); this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE)); this.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR)); + + return {isPromoted, isDemoted}; }, }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts index 9d7a3de4f85..2b258154a52 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts @@ -49,5 +49,68 @@ describe('plugin-meetings', () => { assert.equal(webinar.canManageWebcast, false); }); }); + + describe('#updateRoleChanged', () => { + it('updates roles when promoted from attendee to panelist', () => { + const payload = { + oldRoles: ['ATTENDEE'], + newRoles: ['PANELIST'] + }; + + const result = webinar.updateRoleChanged(payload); + + assert.equal(webinar.selfIsPanelist, true, 'self should be a panelist'); + assert.equal(webinar.selfIsAttendee, false, 'self should not be an attendee'); + assert.equal(webinar.canManageWebcast, false, 'self should not have manage webcast capability'); + assert.equal(result.isPromoted, true, 'should indicate promotion'); + assert.equal(result.isDemoted, false, 'should not indicate demotion'); + }); + + it('updates roles when demoted from panelist to attendee', () => { + const payload = { + oldRoles: ['PANELIST'], + newRoles: ['ATTENDEE'] + }; + + const result = webinar.updateRoleChanged(payload); + + assert.equal(webinar.selfIsPanelist, false, 'self should not be a panelist'); + assert.equal(webinar.selfIsAttendee, true, 'self should be an attendee'); + assert.equal(webinar.canManageWebcast, false, 'self should not have manage webcast capability'); + assert.equal(result.isPromoted, false, 'should not indicate promotion'); + assert.equal(result.isDemoted, true, 'should indicate demotion'); + }); + + it('updates roles when promoted to moderator', () => { + const payload = { + oldRoles: ['PANELIST'], + newRoles: ['MODERATOR'] + }; + + const result = webinar.updateRoleChanged(payload); + + assert.equal(webinar.selfIsPanelist, false, 'self should not be a panelist'); + assert.equal(webinar.selfIsAttendee, false, 'self should not be an attendee'); + assert.equal(webinar.canManageWebcast, true, 'self should have manage webcast capability'); + assert.equal(result.isPromoted, false, 'should not indicate promotion'); + assert.equal(result.isDemoted, false, 'should not indicate demotion'); + }); + + it('updates roles when unchanged (remains as panelist)', () => { + const payload = { + oldRoles: ['PANELIST'], + newRoles: ['PANELIST'] + }; + + const result = webinar.updateRoleChanged(payload); + + assert.equal(webinar.selfIsPanelist, true, 'self should remain a panelist'); + assert.equal(webinar.selfIsAttendee, false, 'self should not be an attendee'); + assert.equal(webinar.canManageWebcast, false, 'self should not have manage webcast capability'); + assert.equal(result.isPromoted, false, 'should not indicate promotion'); + assert.equal(result.isDemoted, false, 'should not indicate demotion'); + }); + }); + }) }) From e172fb2cd3dfc7d6c336d66e11efe40aaee152db Mon Sep 17 00:00:00 2001 From: mickelr Date: Wed, 13 Nov 2024 16:30:02 +0800 Subject: [PATCH 12/16] feat(webinar): fix issues found by coderabbitai --- .../src/controls-options-manager/types.ts | 4 ++-- .../src/locus-info/fullState.ts | 2 +- .../plugin-meetings/src/webinar/index.ts | 8 +++++--- .../test/unit/spec/webinar/index.ts | 20 ++++++++++++++----- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts index dd2b668bb04..5d155b464ed 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/types.ts @@ -36,8 +36,8 @@ export interface VideoProperties { export interface ViewTheParticipantListProperties { enabled?: boolean; - panelistEnabled?: true; - attendeeCount?: false; + panelistEnabled?: boolean; + attendeeCount?: boolean; } export type Properties = diff --git a/packages/@webex/plugin-meetings/src/locus-info/fullState.ts b/packages/@webex/plugin-meetings/src/locus-info/fullState.ts index 4ff7036443e..f207823a675 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/fullState.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/fullState.ts @@ -6,7 +6,7 @@ FullState.parse = (fullState) => ({ type: fullState.type || FULL_STATE.UNKNOWN, meetingState: fullState.state, locked: fullState.locked, - attendeeCount: fullState.attendeeCount, + attendeeCount: typeof fullState.attendeeCount === 'number' ? fullState.attendeeCount : 0, }); FullState.getFullState = (oldFullState, newFullState) => { diff --git a/packages/@webex/plugin-meetings/src/webinar/index.ts b/packages/@webex/plugin-meetings/src/webinar/index.ts index d057ab8fe1b..709ca7f9cde 100644 --- a/packages/@webex/plugin-meetings/src/webinar/index.ts +++ b/packages/@webex/plugin-meetings/src/webinar/index.ts @@ -52,9 +52,11 @@ const Webinar = WebexPlugin.extend({ }, /** - * Update whether self has capability to manage start/stop webcast (only host can manage it) + * Updates user roles and manages associated state transitions * @param {object} payload - * @returns {void} + * @param {string[]} payload.oldRoles - Previous roles of the user + * @param {string[]} payload.newRoles - New roles of the user + * @returns {{isPromoted: boolean, isDemoted: boolean}} Role transition states */ updateRoleChanged(payload) { const isPromoted = @@ -65,7 +67,7 @@ const Webinar = WebexPlugin.extend({ get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE); this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST)); this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE)); - this.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR)); + this.updateCanManageWebcast(get(payload, 'newRoles', []).includes(SELF_ROLES.MODERATOR)); return {isPromoted, isDemoted}; }, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts index 2b258154a52..8a06824220c 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/webinar/index.ts @@ -39,14 +39,24 @@ describe('plugin-meetings', () => { describe('#updateCanManageWebcast', () => { - it('update canManageWebcast', () => { - webinar.updateCanManageWebcast(true); + it('sets the webcast instance url when valid', () => { + webinar.updateWebcastUrl({resources: {webcastInstance: {url:'newUrl'}}}); + assert.equal(webinar.webcastInstanceUrl, 'newUrl', 'webcast instance URL should be updated'); + }); - assert.equal(webinar.canManageWebcast, true); + it('handles missing resources gracefully', () => { + webinar.updateWebcastUrl({}); + assert.isUndefined(webinar.webcastInstanceUrl, 'webcast instance URL should be undefined'); + }); - webinar.updateCanManageWebcast(false); + it('handles missing webcastInstance gracefully', () => { + webinar.updateWebcastUrl({resources: {}}); + assert.isUndefined(webinar.webcastInstanceUrl, 'webcast instance URL should be undefined'); + }); - assert.equal(webinar.canManageWebcast, false); + it('handles missing URL gracefully', () => { + webinar.updateWebcastUrl({resources: {webcastInstance: {}}}); + assert.isUndefined(webinar.webcastInstanceUrl, 'webcast instance URL should be undefined'); }); }); From aebbbc753c2aaaa543c7db8712a10044f0826e67 Mon Sep 17 00:00:00 2001 From: mickelr Date: Wed, 13 Nov 2024 16:58:02 +0800 Subject: [PATCH 13/16] feat(webinar): fix issues found by coderabbitai --- .../src/controls-options-manager/util.ts | 2 -- .../plugin-meetings/src/locus-info/controlsUtils.ts | 10 +++++----- .../test/unit/spec/controls-options-manager/index.js | 4 ++-- .../test/unit/spec/locus-info/index.js | 12 ------------ 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts index b8340a9eb04..0dad61a9938 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/util.ts @@ -220,11 +220,9 @@ class Utils { if (control.properties.panelistEnabled === true) { requiredHints.push(DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST); } - // @ts-ignore if (control.properties.panelistEnabled === false) { requiredHints.push(DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST_PANELIST); } - // @ts-ignore if (control.properties.attendeeCount === true) { requiredHints.push(DISPLAY_HINTS.ENABLE_SHOW_ATTENDEE_COUNT); } diff --git a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts index 7bc0296d6df..81b1734f626 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/controlsUtils.ts @@ -80,9 +80,9 @@ ControlsUtils.parse = (controls: any) => { if (controls?.viewTheParticipantList) { parsedControls.viewTheParticipantList = { - enabled: controls.viewTheParticipantList.enabled, - panelistEnabled: controls.viewTheParticipantList.panelistEnabled, - attendeeCount: controls.viewTheParticipantList.attendeeCount, + enabled: controls.viewTheParticipantList?.enabled ?? false, + panelistEnabled: controls.viewTheParticipantList?.panelistEnabled ?? false, + attendeeCount: controls.viewTheParticipantList?.attendeeCount ?? 0, }; } @@ -100,8 +100,8 @@ ControlsUtils.parse = (controls: any) => { if (controls?.meetingFull) { parsedControls.meetingFull = { - meetingFull: controls.meetingFull.meetingFull, - meetingPanelistFull: controls.meetingFull.meetingPanelistFull, + meetingFull: controls.meetingFull?.meetingFull ?? false, + meetingPanelistFull: controls.meetingFull?.meetingPanelistFull ?? false, }; } diff --git a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js index 00bfe506504..f78893badee 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/controls-options-manager/index.js @@ -95,7 +95,7 @@ describe('plugin-meetings', () => { assert.isRejected(result); }); - it('can set mute on entry when the display hint is available enabled=true', () => { + it('can set disallow unmute when ENABLE_HARD_MUTE display hint is available', () => { manager.setDisplayHints(['ENABLE_HARD_MUTE']); const result = manager.setDisallowUnmute(true); @@ -107,7 +107,7 @@ describe('plugin-meetings', () => { assert.deepEqual(result, request.request.firstCall.returnValue); }); - it('can set mute on entry when the display hint is available enabled=false', () => { + it('can set allow unmute when DISABLE_HARD_MUTE display hint is available', () => { manager.setDisplayHints(['DISABLE_HARD_MUTE']); const result = manager.setDisallowUnmute(false); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js index 1150e22b553..ecfe2b38afe 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js @@ -194,18 +194,6 @@ describe('plugin-meetings', () => { ); }); - it('should trigger the CONTROLS_VIDEO_CHANGED event when necessary', () => { - locusInfo.controls = {}; - locusInfo.emitScoped = sinon.stub(); - locusInfo.updateControls(newControls); - - assert.calledWith( - locusInfo.emitScoped, - {file: 'locus-info', function: 'updateControls'}, - LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, - {state: newControls.video} - ); - }); it('should trigger the CONTROLS_VIDEO_CHANGED event when necessary', () => { locusInfo.controls = {}; locusInfo.emitScoped = sinon.stub(); From 2a24f75663a0541a7dc1352ff7751bdee5870d4e Mon Sep 17 00:00:00 2001 From: mickelr Date: Thu, 14 Nov 2024 09:36:51 +0800 Subject: [PATCH 14/16] feat(webinar): fix issues found by coderabbitai --- .../plugin-meetings/src/controls-options-manager/index.ts | 7 ++++++- packages/@webex/plugin-meetings/src/meeting/index.ts | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts index 73a383fdc42..92ad7db2604 100644 --- a/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts +++ b/packages/@webex/plugin-meetings/src/controls-options-manager/index.ts @@ -177,7 +177,12 @@ export default class ControlsOptionsManager { * @memberof ControlsOptionsManager * @returns {Promise} */ - private setControls(setting: {[key in Setting]?: any}): Promise { + private setControls(setting: { + [Setting.muted]?: boolean; + [Setting.disallowUnmute]?: boolean; + [Setting.muteOnEntry]?: boolean; + [Setting.roles]?: Array; + }): Promise { LoggerProxy.logger.log( `ControlsOptionsManager:index#setControls --> ${JSON.stringify(setting)}` ); diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index c04f380c658..e067e66837e 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -3017,8 +3017,7 @@ export default class Meeting extends StatelessWebexPlugin { /** * Set up the locus info resources link listener * update the locusInfo for webcast instance url - * does not currently re-emit the event as it's internal only - * payload is unused + * @param {Object} payload - The event payload * @returns {undefined} * @private * @memberof Meeting From a3b447d8e9fbaac9e231c986c8907e938d7be959 Mon Sep 17 00:00:00 2001 From: mickelr Date: Thu, 14 Nov 2024 15:21:32 +0800 Subject: [PATCH 15/16] feat(webinar): update lower hand API and unit test --- .../plugin-meetings/src/members/index.ts | 6 ++- .../plugin-meetings/src/members/util.ts | 5 ++- .../test/unit/spec/members/index.js | 27 ++++++++++++- .../test/unit/spec/members/request.js | 40 +++++++++++++++++-- .../test/unit/spec/members/utils.js | 16 +++++++- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/members/index.ts b/packages/@webex/plugin-meetings/src/members/index.ts index e29cd2d50cb..f709100b3a4 100644 --- a/packages/@webex/plugin-meetings/src/members/index.ts +++ b/packages/@webex/plugin-meetings/src/members/index.ts @@ -915,11 +915,12 @@ export default class Members extends StatelessWebexPlugin { /** * Lower all hands of members in a meeting * @param {String} requestingMemberId - id of the participant which requested the lower all hands + * @param {array} roles which should be lowered * @returns {Promise} * @public * @memberof Members */ - public lowerAllHands(requestingMemberId: string) { + public lowerAllHands(requestingMemberId: string, roles: Array) { if (!this.locusUrl) { return Promise.reject( new ParameterError( @@ -936,7 +937,8 @@ export default class Members extends StatelessWebexPlugin { } const options = MembersUtil.generateLowerAllHandsMemberOptions( requestingMemberId, - this.locusUrl + this.locusUrl, + roles ); return this.membersRequest.lowerAllHandsMember(options); diff --git a/packages/@webex/plugin-meetings/src/members/util.ts b/packages/@webex/plugin-meetings/src/members/util.ts index bd491145c0d..fec1930424f 100644 --- a/packages/@webex/plugin-meetings/src/members/util.ts +++ b/packages/@webex/plugin-meetings/src/members/util.ts @@ -14,6 +14,7 @@ import { import {RoleAssignmentOptions, RoleAssignmentRequest, ServerRoleShape} from './types'; +// @ts-ignore const MembersUtil = { /** * @param {Object} invitee with emailAddress, email or phoneNumber @@ -166,9 +167,10 @@ const MembersUtil = { locusUrl, }), - generateLowerAllHandsMemberOptions: (requestingParticipantId, locusUrl) => ({ + generateLowerAllHandsMemberOptions: (requestingParticipantId, locusUrl, roles) => ({ requestingParticipantId, locusUrl, + ...(roles !== undefined && {roles}), }), /** @@ -253,6 +255,7 @@ const MembersUtil = { const body = { hand: { raised: false, + ...(options.roles !== undefined && {roles: options.roles}), }, requestingParticipantId: options.requestingParticipantId, }; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/index.js b/packages/@webex/plugin-meetings/test/unit/spec/members/index.js index 64421f2df8c..23627ab151b 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/index.js @@ -660,17 +660,20 @@ describe('plugin-meetings', () => { resultPromise, spies, expectedRequestingMemberId, - expectedLocusUrl + expectedLocusUrl, + expectedRoles, ) => { await assert.isFulfilled(resultPromise); assert.calledOnceWithExactly( spies.generateLowerAllHandsMemberOptions, expectedRequestingMemberId, - expectedLocusUrl + expectedLocusUrl, + expectedRoles, ); assert.calledOnceWithExactly(spies.lowerAllHandsMember, { requestingParticipantId: expectedRequestingMemberId, locusUrl: expectedLocusUrl, + ...(expectedRoles !== undefined && { roles: expectedRoles }) }); assert.strictEqual(resultPromise, spies.lowerAllHandsMember.getCall(0).returnValue); }; @@ -707,6 +710,26 @@ describe('plugin-meetings', () => { await checkValid(resultPromise, spies, requestingMemberId, url1); }); + + it('should make the correct request when called with valid requestingMemberId and roles', async () => { + const requestingMemberId = 'test-member-id'; + const roles = ['panelist', 'attendee']; + const { members, spies } = setup('test-locus-url'); + + const resultPromise = members.lowerAllHands(requestingMemberId, roles); + + await checkValid(resultPromise, spies, requestingMemberId, 'test-locus-url', roles); + }); + + it('should handle an empty roles array correctly', async () => { + const requestingMemberId = 'test-member-id'; + const roles = []; + const { members, spies } = setup('test-locus-url'); + + const resultPromise = members.lowerAllHands(requestingMemberId, roles); + + await checkValid(resultPromise, spies, requestingMemberId, 'test-locus-url', roles); + }); }); describe('#editDisplayName', () => { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/request.js b/packages/@webex/plugin-meetings/test/unit/spec/members/request.js index e1fe2b3606c..c743c2fc6a9 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/request.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/request.js @@ -225,7 +225,7 @@ describe('plugin-meetings', () => { }); describe('#assignRolesMember', () => { - it('sends a PATCH to the locus endpoint', async () => { + it('sends a assignRolesMember PATCH to the locus endpoint', async () => { const locusUrl = url1; const memberId = 'test1'; const roles = [ @@ -255,7 +255,7 @@ describe('plugin-meetings', () => { }); describe('#raiseHand', () => { - it('sends a PATCH to the locus endpoint', async () => { + it('sends a raiseOrLowerHandMember PATCH to the locus endpoint', async () => { const locusUrl = url1; const memberId = 'test1'; @@ -319,7 +319,7 @@ describe('plugin-meetings', () => { assert.strictEqual(result, requestResponse); }); - it('sends a PATCH to the locus endpoint', async () => { + it('sends a lowerAllHandsMember PATCH to the locus endpoint', async () => { const locusUrl = url1; const memberId = 'test1'; @@ -348,6 +348,40 @@ describe('plugin-meetings', () => { }, }); }); + + it('sends a lowerAllHandsMember PATCH to the locus endpoint with roles', async () => { + const locusUrl = url1; + const memberId = 'test1'; + const roles = ['attendee']; + + const options = { + requestingParticipantId: memberId, + locusUrl, + roles, + }; + + const getRequestParamsSpy = sandbox.spy(membersUtil, 'getLowerAllHandsMemberRequestParams'); + + await membersRequest.lowerAllHandsMember(options); + + assert.calledOnceWithExactly(getRequestParamsSpy, { + requestingParticipantId: memberId, + locusUrl: url1, + roles: ['attendee'], + }); + + checkRequest({ + method: 'PATCH', + uri: `${locusUrl}/controls`, + body: { + hand: { + raised: false, + roles: ['attendee'], + }, + requestingParticipantId: memberId, + }, + }); + }); }); describe('#editDisplayName', () => { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js index 1ce608fe8a0..c5477c8b340 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js @@ -101,7 +101,7 @@ describe('plugin-meetings', () => { }); }); describe('#generateLowerAllHandsMemberOptions', () => { - it('returns the correct options', () => { + it('returns the correct options without roles', () => { const requestingParticipantId = 'test'; const locusUrl = 'urlTest1'; @@ -113,6 +113,20 @@ describe('plugin-meetings', () => { } ); }); + it('returns the correct options with roles', () => { + const requestingParticipantId = 'test'; + const locusUrl = 'urlTest1'; + const roles = ['panelist']; + + assert.deepEqual( + MembersUtil.generateLowerAllHandsMemberOptions(requestingParticipantId, locusUrl, roles), + { + requestingParticipantId, + locusUrl, + roles, + } + ); + }); }); describe('#generateEditDisplayNameMemberOptions', () => { it('returns the correct options', () => { From 952c5202fd9660f64b254c36447125ec3246cbe8 Mon Sep 17 00:00:00 2001 From: mickelr Date: Fri, 15 Nov 2024 10:09:40 +0800 Subject: [PATCH 16/16] feat(webinar): update readme --- packages/@webex/plugin-meetings/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@webex/plugin-meetings/README.md b/packages/@webex/plugin-meetings/README.md index 52d211515c2..4a3568f9747 100644 --- a/packages/@webex/plugin-meetings/README.md +++ b/packages/@webex/plugin-meetings/README.md @@ -857,7 +857,8 @@ meeting.members.raiseOrLowerHand(memberId); // You can lower all hands in a meeting // use a memberId string to indicate who is requesting lowering all hands -meeting.members.lowerAllHands(requestingMemberId); +// (optional) use a roles array to indicate who should have their hands lowered, default to all roles +meeting.members.lowerAllHands(requestingMemberId, roles); // You can transfer the host role to another member in the meeting, this is proxied by meeting.transfer // use a memberId string and a moderator boolean to transfer or not, default to true