diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index e9c5c5ce57f..433a59afede 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -1086,6 +1086,7 @@ export const DISPLAY_HINTS = { // AI ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED: 'ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED', + ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL: 'ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL', }; export const INTERSTITIAL_DISPLAY_HINTS = [DISPLAY_HINTS.VOIP_IS_ENABLED]; 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 c750a1a8525..fd475c3e190 100644 --- a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts @@ -118,6 +118,7 @@ interface IInMeetingActions { canEnablePollingQA?: boolean; canDisablePollingQA?: boolean; canAttendeeRequestAiAssistantEnabled?: boolean; + isAttendeeRequestAiAssistantDeclinedAll?: boolean; } /** @@ -340,6 +341,8 @@ export default class InMeetingActions implements IInMeetingActions { canAttendeeRequestAiAssistantEnabled = null; + isAttendeeRequestAiAssistantDeclinedAll = null; + /** * Returns all meeting action options * @returns {Object} @@ -452,6 +455,7 @@ export default class InMeetingActions implements IInMeetingActions { canEnablePollingQA: this.canEnablePollingQA, canDisablePollingQA: this.canDisablePollingQA, canAttendeeRequestAiAssistantEnabled: this.canAttendeeRequestAiAssistantEnabled, + isAttendeeRequestAiAssistantDeclinedAll: this.isAttendeeRequestAiAssistantDeclinedAll, }); /** diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 38b31ae8f09..78d92d9f148 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -4553,6 +4553,8 @@ export default class Meeting extends StatelessWebexPlugin { this.userDisplayHints, this.roles ), + isAttendeeRequestAiAssistantDeclinedAll: + MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints), }) || changed; } if (changed) { diff --git a/packages/@webex/plugin-meetings/src/meeting/util.ts b/packages/@webex/plugin-meetings/src/meeting/util.ts index d06afa81134..9b2ae251049 100644 --- a/packages/@webex/plugin-meetings/src/meeting/util.ts +++ b/packages/@webex/plugin-meetings/src/meeting/util.ts @@ -919,6 +919,9 @@ const MeetingUtil = { return false; }, + attendeeRequestAiAssistantDeclinedAll: (displayHints = []) => + displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL), + selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record) => { if (!userPolicies) { return true; 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 bb46def9a2c..fd51a90839d 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 @@ -112,6 +112,7 @@ describe('plugin-meetings', () => { canEnablePollingQA: null, canDisablePollingQA: null, canAttendeeRequestAiAssistantEnabled: null, + isAttendeeRequestAiAssistantDeclinedAll: null, ...expected, }; @@ -230,6 +231,7 @@ describe('plugin-meetings', () => { 'canEnablePollingQA', 'canDisablePollingQA', 'canAttendeeRequestAiAssistantEnabled', + 'isAttendeeRequestAiAssistantDeclinedAll', ].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 29cf504e496..3868b98658d 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -265,7 +265,7 @@ describe('plugin-meetings', () => { stopReachability: sinon.stub(), isSubnetReachable: sinon.stub().returns(true), }; - webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false) + webex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false); webex.internal.llm.on = sinon.stub(); webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies( {}, @@ -11899,6 +11899,7 @@ describe('plugin-meetings', () => { let isSpokenLanguageAutoDetectionEnabledSpy; let showAutoEndMeetingWarningSpy; let canAttendeeRequestAiAssistantEnabledSpy; + let attendeeRequestAiAssistantDeclinedAllSpy; // Due to import tree issues, hasHints must be stubed within the scope of the `it`. beforeEach(() => { @@ -11939,6 +11940,10 @@ describe('plugin-meetings', () => { MeetingUtil, 'canAttendeeRequestAiAssistantEnabled' ); + attendeeRequestAiAssistantDeclinedAllSpy = sinon.spy( + MeetingUtil, + 'attendeeRequestAiAssistantDeclinedAll' + ); }); afterEach(() => { @@ -11946,6 +11951,7 @@ describe('plugin-meetings', () => { waitingForOthersToJoinSpy.restore(); showAutoEndMeetingWarningSpy.restore(); canAttendeeRequestAiAssistantEnabledSpy.restore(); + attendeeRequestAiAssistantDeclinedAllSpy.restore(); }); forEach( @@ -12501,6 +12507,7 @@ describe('plugin-meetings', () => { userDisplayHints, meeting.roles ); + assert.calledWith(attendeeRequestAiAssistantDeclinedAllSpy, userDisplayHints); assert.calledWith(ControlsOptionsUtil.hasHints, { requiredHints: [DISPLAY_HINTS.MUTE_ALL], @@ -12709,7 +12716,7 @@ describe('plugin-meetings', () => { meeting.joinedWith = {state: 'JOINED'}; meeting.locusInfo = { url: 'a url', - info: {datachannelUrl: 'a datachannel url'} + info: {datachannelUrl: 'a datachannel url'}, }; const result = await meeting.updateLLMConnection(); @@ -12757,7 +12764,7 @@ describe('plugin-meetings', () => { ); assert.equal(result, 'something'); }); - it('disconnects if the locus url has changed', async () => { + it('disconnects if the locus url has changed', async () => { meeting.joinedWith = {state: 'JOINED'}; webex.internal.llm.isConnected.returns(true); @@ -12766,15 +12773,15 @@ describe('plugin-meetings', () => { meeting.locusInfo = { url: 'a different url', info: {datachannelUrl: 'a datachannel url'}, - self: {} + self: {}, }; const result = await meeting.updateLLMConnection(); - assert.calledWithExactly( - webex.internal.llm.disconnectLLM, - {code: 3050, reason: 'done (permanent)'} - ); + assert.calledWithExactly(webex.internal.llm.disconnectLLM, { + code: 3050, + reason: 'done (permanent)', + }); assert.calledWithExactly( webex.internal.llm.registerAndConnect, @@ -12816,15 +12823,15 @@ describe('plugin-meetings', () => { meeting.locusInfo = { url: 'a url', info: {datachannelUrl: 'a different datachannel url'}, - self: {} + self: {}, }; const result = await meeting.updateLLMConnection(); - assert.calledWithExactly( - webex.internal.llm.disconnectLLM, - {code: 3050, reason: 'done (permanent)'} - ); + assert.calledWithExactly(webex.internal.llm.disconnectLLM, { + code: 3050, + reason: 'done (permanent)', + }); assert.calledWithExactly( webex.internal.llm.registerAndConnect, @@ -12911,11 +12918,7 @@ describe('plugin-meetings', () => { 'a datachannel url', 'token-123' ); - assert.calledWithExactly( - webex.internal.llm.setDatachannelToken, - 'token-123', - 'default' - ); + assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default'); }); it('prefers refreshed token over locus self token', async () => { meeting.joinedWith = {state: 'JOINED'}; @@ -12925,9 +12928,7 @@ describe('plugin-meetings', () => { self: {datachannelToken: 'locus-token'}, }; - webex.internal.llm.getDatachannelToken - .withArgs('default') - .returns('refreshed-token'); + webex.internal.llm.getDatachannelToken.withArgs('default').returns('refreshed-token'); await meeting.updateLLMConnection(); @@ -12958,8 +12959,10 @@ describe('plugin-meetings', () => { meeting.webinar.isJoinPracticeSessionDataChannel.returns(true); webex.internal.llm.getDatachannelToken - .withArgs(true).returns('refreshed-ps-token') // refreshed practice token - .withArgs(false).returns('refreshed-normal-token'); // refreshed normal token + .withArgs(true) + .returns('refreshed-ps-token') // refreshed practice token + .withArgs(false) + .returns('refreshed-normal-token'); // refreshed normal token await meeting.updateLLMConnection(); @@ -12981,7 +12984,7 @@ describe('plugin-meetings', () => { meeting.locusInfo = { url: 'a url', info: {datachannelUrl: 'a datachannel url'}, - self: {datachannelToken: 'token-123'} + self: {datachannelToken: 'token-123'}, }; webex.internal.llm.getDatachannelToken.returns(undefined); @@ -12995,11 +12998,7 @@ describe('plugin-meetings', () => { 'a datachannel url', 'token-123' ); - assert.calledWithExactly( - webex.internal.llm.setDatachannelToken, - 'token-123', - 'default' - ); + assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default'); }); }); @@ -14577,7 +14576,7 @@ describe('plugin-meetings', () => { meeting.locusUrl = 'https://locus.example.com'; meeting.meetingRequest = { fetchDatachannelToken: sinon.stub().resolves({ - body: { datachannelToken: 'mock-token' }, + body: {datachannelToken: 'mock-token'}, }), }; meeting.members = { @@ -14593,14 +14592,11 @@ describe('plugin-meetings', () => { sinon.assert.calledOnce(meeting.meetingRequest.fetchDatachannelToken); - sinon.assert.calledWith( - meeting.meetingRequest.fetchDatachannelToken, - { - locusUrl: 'https://locus.example.com', - requestingParticipantId: 'self-123', - isPracticeSession: true, - } - ); + sinon.assert.calledWith(meeting.meetingRequest.fetchDatachannelToken, { + locusUrl: 'https://locus.example.com', + requestingParticipantId: 'self-123', + isPracticeSession: true, + }); }); it('returns the correct structured result', async () => { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js index a639f2d7427..ee057e26782 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js @@ -1011,6 +1011,30 @@ describe('plugin-meetings', () => { }); }); + describe('attendeeRequestAiAssistantDeclinedAll', () => { + it('returns true when display hint is present', () => { + assert.isTrue( + MeetingUtil.attendeeRequestAiAssistantDeclinedAll([ + 'ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL', + ]) + ); + }); + + it('returns false when display hint is not present', () => { + assert.isFalse(MeetingUtil.attendeeRequestAiAssistantDeclinedAll([])); + }); + + it('returns false when display hint is absent among other hints', () => { + assert.isFalse( + MeetingUtil.attendeeRequestAiAssistantDeclinedAll(['SOME_OTHER_HINT', 'ANOTHER_HINT']) + ); + }); + + it('returns false when called with no arguments', () => { + assert.isFalse(MeetingUtil.attendeeRequestAiAssistantDeclinedAll()); + }); + }); + describe('bothLeaveAndEndMeetingAvailable', () => { it('works as expected', () => { assert.deepEqual(