UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

1,149 lines (1,130 loc) • 3.54 MB
'use strict'; var communicationCommon = require('@azure/communication-common'); var reselect = require('reselect'); var communicationCalling = require('@azure/communication-calling'); var memoizeOne = require('memoize-one'); var logger = require('@azure/logger'); var events = require('events'); var immer = require('immer'); var React = require('react'); var react = require('@fluentui/react'); var reactComponents = require('@fluentui/react-components'); var reactIcons = require('@fluentui/react-icons'); var uuid = require('uuid'); var reactChat = require('@fluentui-contrib/react-chat'); var react$1 = require('@griffel/react'); var parse = require('html-react-parser'); var Linkify = require('react-linkify'); var DOMPurify = require('dompurify'); var reactFileTypeIcons = require('@fluentui/react-file-type-icons'); var reactHooks = require('@fluentui/react-hooks'); var reactUseDraggableScroll = require('react-use-draggable-scroll'); var copy = require('copy-to-clipboard'); var communicationChat = require('@azure/communication-chat'); var nanoid = require('nanoid'); var communicationCallingEffects = require('@azure/communication-calling-effects'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var reselect__namespace = /*#__PURE__*/_interopNamespaceDefault(reselect); var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. const argsCmp = (args1, args2, objCmp) => { return args1.length === args2.length && args1.every((arg1, index) => objCmp(args2[index], arg1)); }; /** * The function memoize a series of function calls in a single pass, * it memoizes all the args and return in a single run of the callback function, and read it in the next round of execution * note: this is a memory opimized function which will only memoize one round of bulk calls * @param fnToMemoize - the function needs to be bulk memorized and a key key paramter needs to be provided as cache id * @param shouldCacheUpdate - the validate function for comparing 2 argument, return true when 2 args are equal * @returns callback function includes a series calls of memoizedFn, and each call will get cache result if args are the same(according to shouldCacheUpdate fn) * @example * ```ts * const items = [{id:1, value:3}]; * const heavyFn = (_key, value) => { // key is not used in the function, but it is a cache id * // assume this is a heavy caculation * return value+1; * } * * const memoizeHeavyFnAll = memoizeFnAll(heavyFn); * const generateValueArray = (memoizedHeavyFn) => ( * items.map(item => { * memoizedHeavyFn(item.id, item.value); * }) * ); * * const result = memoizeHeavyFnAll(generateValueArray); // Cache: {}, nextCache: {1: 4 *new}, heavyFn call times: 1 * * // Argument changed * items[0].value = 2 * const result0 = memoizeHeavyFnAll(generateValueArray); // Cache: {1: 4}, nextCache: {1: 3 *new}, heavyFn call times: 1 * * // Cache added * items.push({id:3, value:4}); * const result1 = memoizeHeavyFnAll(generateValueArray); // Cache: {1: 3 *hit}, nextCache: {1: 3, 3: 5 *new}, heavyFn call times: 1 * * // Cache removed * delete items[0]; * const result2 = memoizeHeavyFnAll(generateValueArray); // Cache: {1: 3, 3: 5 *hit}, nextCache: {3: 5}, heavyFn call times: 0 * ``` * * @public */ const memoizeFnAll = (fnToMemoize, shouldCacheUpdate = Object.is) => { let cache = new Map(); let nextCache = new Map(); return (callback) => { const memoizedFn = (key, ...args) => { const value = cache.get(key); if (value) { const [preArgs, ret] = value; if (argsCmp(preArgs, args, shouldCacheUpdate)) { nextCache.set(key, [args, ret]); return ret; } } const ret = fnToMemoize(key, ...args); nextCache.set(key, [args, ret]); return ret; }; const retValue = callback(memoizedFn); cache = nextCache; nextCache = new Map(); return retValue; }; }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * A string representation of a {@link @azure/communication-common#CommunicationIdentifier}. * * This string representation of CommunicationIdentifier is guaranteed to be stable for * a unique Communication user. Thus, * - it can be used to persist a user's identity in external databases. * - it can be used as keys into a Map to store data for the user. * * @public */ const toFlatCommunicationIdentifier = (identifier) => { return communicationCommon.getIdentifierRawId(identifier); }; /** * Reverse operation of {@link toFlatCommunicationIdentifier}. * * @public */ const fromFlatCommunicationIdentifier = (id) => { // if the id passed is a phone number we need to build the rawId to pass in const rawId = id.indexOf('+') === 0 ? '4:' + id : id; return communicationCommon.createIdentifierFromRawId(rawId); }; /** * Returns a CommunicationIdentifier. * @internal */ const _toCommunicationIdentifier = (id) => { if (typeof id === 'string') { return fromFlatCommunicationIdentifier(id); } return id; }; /** * Check if an object is identifier. * * @internal */ const _isValidIdentifier = (identifier) => { return communicationCommon.isCommunicationUserIdentifier(identifier) || communicationCommon.isPhoneNumberIdentifier(identifier) || communicationCommon.isMicrosoftTeamsUserIdentifier(identifier) || communicationCommon.isUnknownIdentifier(identifier); }; /** * Check if given identifier is a Microsoft Teams user. * * @internal * @param rawId - The rawId of the identifier. * @returns True if the identifier is a Microsoft Teams user. False otherwise. */ const _isIdentityMicrosoftTeamsUser = (rawId) => { if (!rawId) { return false; } const identifier = _toCommunicationIdentifier(rawId); return communicationCommon.isMicrosoftTeamsUserIdentifier(identifier); }; function getDefaultExportFromCjs (x) { return x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // GENERATED FILE. DO NOT EDIT MANUALLY. var telemetryVersion = '1.30.0'; var telemetryVersion$1 = /*@__PURE__*/getDefaultExportFromCjs(telemetryVersion); // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @private */ // Removes long suffixes that don't fit the constraints for telemetry application ID. // e.g., the build suffix is dropped for alpha package versions. const sanitize = (version) => { const alphaIndex = version.search(/alpha/); if (alphaIndex >= 0) { return version.substring(0, alphaIndex + 5); } return version; }; /** * Takes a telemetry implementation hint and returns the numerical value. * * @private */ const getTelemetryImplementationHint = (telemetryImplementationHint) => { switch (telemetryImplementationHint) { case 'Call': return 1; case 'Chat': return 2; case 'CallWithChat': return 3; case 'StatefulComponents': return 4; default: return 0; } }; /** * Application ID to be included in telemetry data from the UI library. * Template: acXYYY/<version> * Where: * - X describes a platform, [r: web, i: iOS, a: Android] * - YYY describes what's running on this platform (optional, currently unused by this library): * Y[0] is high-level artifact, * [0: undefined, 1: AzureCommunicationLibrary, 2: ACS SampleApp] * Y[1] is specific implementation, * [0: undefined, 1: Call Composite, 2: Chat Composite, 3: CallWithChatComposite, 4: UI Components] * Y[2] is reserved for implementation details, * [0: undefined] * * @internal */ const _getApplicationId = (telemetryImplementationHint) => { // We assume AzureCommunicationLibrary, as we don't currently have any public API to inject otherwise. // This is consistent with the native iOS and Android implementations of telemetry. const highLevelArtifact = 1; const specificImplementation = getTelemetryImplementationHint(telemetryImplementationHint); const implementationDetails = 0; const version = telemetryVersion$1; return sanitize(`acr${highLevelArtifact}${specificImplementation}${implementationDetails}/${version}`); }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @internal * * Replace the pattern "\{\}" in str with the values passed in as vars * * @example * ```ts * _formatString("hello {name}. '{name}' is a rare name.", {name: "Foo"}); * // returns "hello Foo. 'Foo' is a rare name." * ``` * @param str - The string to be formatted * @param variables - Variables to use to format the string * @returns a formatted string */ const _formatString = (str, vars) => { if (!str) { return ''; } if (!vars) { return str; } // regex to search for the pattern "\{\}" const placeholdersRegex = /{(\w+)}/g; return str.replace(placeholdersRegex, (_, k) => { const replaceValue = vars[k]; return replaceValue === undefined ? `{${k}}` : replaceValue; }); }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * Wrap JSON.stringify in a try-catch as JSON.stringify throws an exception if it fails. * * Use this only in areas where the JSON.stringify is non-critical and OK for the JSON.stringify to fail, such as logging. * * @internal */ const _safeJSONStringify = (value, replacer = createSafeReplacer(), space) => { try { return JSON.stringify(value, replacer, space); } catch (e) { console.error(e); return undefined; } }; // Log all visited refs to avoid circular ref const createSafeReplacer = () => { const visited = new Set(); return function replacer(key, value) { if (typeof value !== 'object') { return value; } if (visited.has(value)) { return 'Visited-Ref'; } else { visited.add(value); return value; } }; }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @internal * Converts units of rem to units of pixels * @param rem - units of rem * @returns units of pixels */ const _convertRemToPx = (rem) => { return rem * getBrowserFontSizeInPx(); }; /** * @internal * Converts units of pixels to units of rem * @param px - units of px * @returns units of rem */ const _convertPxToRem = (px) => { return px / getBrowserFontSizeInPx(); }; const getBrowserFontSizeInPx = () => { let fontSizeInPx = parseFloat(getComputedStyle(document.documentElement).fontSize); // If browser font size is not a number, default to 16 px if (Number.isNaN(fontSizeInPx)) { fontSizeInPx = 16; } return fontSizeInPx; }; /** * @internal * Disable dismiss on resize to work around a couple Fluent UI bugs * - The Callout is dismissed whenever *any child of window (inclusive)* is resized. In practice, this * happens when we change the VideoGallery layout, or even when the video stream element is internally resized * by the headless SDK. * - We also want to prevent dismiss when chat pane is scrolling especially a new message is added. * A side effect of this workaround is that the context menu stays open when window is resized, and may * get detached from original target visually. That bug is preferable to the bug when this value is not set - * The Callout (frequently) gets dismissed automatically. */ const _preventDismissOnEvent = (ev) => { return ev.type === 'resize' || ev.type === 'scroll'; }; /** * @internal * Helper function to get the keys of an object */ function _getKeys(obj) { return Object.keys(obj); } // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @internal * Converts px value to rem value. * For example, an input of `16` will return `1rem`. */ const _pxToRem = (px) => `${px / 16}rem`; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @internal * This is a log function to log structural data for easier parse in telemetry */ const _logEvent = (logger, event) => { logger[event.level](_safeJSONStringify(event)); }; /** * @private */ const getDeviceManager$1 = (state) => state.deviceManager; /** * @private */ const getRole$1 = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.role; }; /** * @private */ const isHideAttendeeNamesEnabled = (state, props) => { var _a, _b; return (_b = (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.hideAttendeeNames) !== null && _b !== void 0 ? _b : false; }; /** * @private */ const getCapabilities = (state, props) => { var _a, _b; return (_b = (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.capabilitiesFeature) === null || _b === void 0 ? void 0 : _b.capabilities; }; /** * @private */ const getCallExists = (state, props) => !!state.calls[props.callId]; /** * @private */ const getDominantSpeakers = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.dominantSpeakers; }; /** * @private */ const getRemoteParticipants$1 = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.remoteParticipants; }; /** * @private */ const getRemoteParticipantsEnded = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.remoteParticipantsEnded; }; /** * @private */ const getLocalParticipantRaisedHand$1 = (state, props) => { var _a, _b; return (_b = (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.raiseHand) === null || _b === void 0 ? void 0 : _b.localParticipantRaisedHand; }; /** * @private */ const getSpotlightCallFeature = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.spotlight; }; /** * @private */ const getLocalParticipantReactionState = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.localParticipantReaction; }; /** * @private */ const getIsScreenSharingOn = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.isScreenSharingOn; }; /** * @private */ const getIsMuted = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.isMuted; }; /** * @private */ const getOptimalVideoCount = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.optimalVideoCount.maxRemoteVideoStreams; }; /** * @private */ const getLocalVideoStreams$1 = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.localVideoStreams; }; /** * @private */ const getScreenShareRemoteParticipant = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.screenShareRemoteParticipant; }; /** * @private */ const getDisplayName$2 = (state) => { var _a; return (_a = state.callAgent) === null || _a === void 0 ? void 0 : _a.displayName; }; /** * @private */ const getIdentifier = (state) => toFlatCommunicationIdentifier(state.userId); /** * @private */ const getLatestErrors$1 = (state) => state.latestErrors; /** * @private */ const getLatestNotifications$1 = (state) => state.latestNotifications; /** * @private */ const getDiagnostics = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.diagnostics; }; /** * @private */ const getCallState = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.state; }; /** * @private */ const getEnvironmentInfo$1 = (state) => { return state.environmentInfo; }; /** @private */ const getParticipantCount = (state, props) => { return undefined; }; /** @private */ const getCaptions = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.captions; }; /** @private */ const getCaptionsStatus$1 = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.isCaptionsFeatureActive; }; /** @private */ const getStartCaptionsInProgress = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.startCaptionsInProgress; }; /** @private */ const getCurrentCaptionLanguage = (state, props) => { var _a, _b; // we default to 'en' if the currentCaptionLanguage is not set if (((_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.currentCaptionLanguage) === '') { return 'en'; } return (_b = state.calls[props.callId]) === null || _b === void 0 ? void 0 : _b.captionsFeature.currentCaptionLanguage; }; /** @private */ const getCurrentSpokenLanguage = (state, props) => { var _a; // when currentSpokenLanguage is '', it means the spoken language is not set. // In this case we suggest showing the captions modal to set the spoken language when starting captions return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.currentSpokenLanguage; }; /** @private */ const getSupportedCaptionLanguages = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.supportedCaptionLanguages; }; /** @private */ const getSupportedSpokenLanguages = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.captionsFeature.supportedSpokenLanguages; }; /** @private */ const getMeetingConferencePhones = (state, props) => { var _a, _b; return (_b = (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.meetingConference) === null || _b === void 0 ? void 0 : _b.conferencePhones; }; /** * selector for retrieving the incoming calls from state * @returns the incoming calls in the call client state * @private */ const getIncomingCalls = (state) => { return Object.values(state.incomingCalls); }; /** * selector for retrieving the incoming calls that have been removed from state * @returns the incoming calls that have been removed * @private */ const getRemovedIncomingCalls = (state) => { return Object.values(state.incomingCallsEnded); }; /** @private */ const getAssignedBreakoutRoom$1 = (state, props) => { var _a, _b; return (_b = (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.breakoutRooms) === null || _b === void 0 ? void 0 : _b.assignedBreakoutRoom; }; /** @private */ const getRealTimeTextStatus = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.realTimeTextFeature.isRealTimeTextFeatureActive; }; /** @private */ const getRealTimeText = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.realTimeTextFeature.realTimeTexts; }; /** * @private */ const getTogetherModeCallFeature = (state, props) => { var _a; return (_a = state.calls[props.callId]) === null || _a === void 0 ? void 0 : _a.togetherMode; }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter$$ = (window && window.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * Check if the call state represents being in the call * * @internal */ const _isInCall = (callStatus) => !!callStatus && !['None', 'Disconnected', 'Connecting', 'Ringing', 'EarlyMedia', 'Disconnecting'].includes(callStatus); /** * Check if the call state represents being in the lobby or waiting to be admitted. * * @internal */ const _isInLobbyOrConnecting = (callStatus) => !!callStatus && ['Connecting', 'Ringing', 'InLobby', 'EarlyMedia'].includes(callStatus); /** * Check if the device manager local video is on when not part of a call * i.e. do unparented views exist. * * @internal */ const _isPreviewOn = (deviceManager) => { var _a; // TODO: we should take in a LocalVideoStream that developer wants to use as their 'Preview' view. We should also // handle cases where 'Preview' view is in progress and not necessary completed. return ((_a = deviceManager.unparentedViews[0]) === null || _a === void 0 ? void 0 : _a.view) !== undefined; }; /** * Dispose of all preview views * We assume all unparented views are local preview views. * * @private */ const disposeAllLocalPreviewViews = (callClient) => __awaiter$$(void 0, void 0, void 0, function* () { const unparentedViews = callClient.getState().deviceManager.unparentedViews; for (const view of unparentedViews) { yield callClient.disposeView(undefined, undefined, view); } }); /** * Update the users displayNames based on the type of user they are * * @internal */ const _updateUserDisplayNames = (participants) => { if (participants) { return memoizedUpdateDisplayName(memoizedFn => { return Object.values(participants).map(p => { const pid = toFlatCommunicationIdentifier(p.identifier); return memoizedFn(pid, p); }); }); } else { return []; } }; /** * Get the display name of a participant for given userId * * @internal * */ const getRemoteParticipantDisplayName = (participantUserId, remoteParticipants) => { let displayName; if (remoteParticipants) { const participant = remoteParticipants[participantUserId]; if (participant) { displayName = participant.displayName; } } return displayName; }; const memoizedUpdateDisplayName = memoizeFnAll((participantId, participant) => { if (communicationCommon.isPhoneNumberIdentifier(participant.identifier)) { return Object.assign(Object.assign({}, participant), { displayName: participant.identifier.phoneNumber }); } else { return participant; } }); /** * @private * A type guard to ensure all participants are acceptable type for Teams call */ const isTeamsCallParticipants = (participants) => { return participants.every(p => !communicationCommon.isCommunicationUserIdentifier(p)); }; /** * @private * Checks whether the user is a 'Ringing' PSTN user or in a 'Connecting' state. */ const _convertParticipantState = (participant) => { return communicationCommon.isPhoneNumberIdentifier(participant.identifier) && participant.state === 'Connecting' ? 'Ringing' : participant.state; }; /** * @private * Changes the display name of the participant based on the local and remote user's role. */ const maskDisplayNameWithRole = (displayName, localUserRole, participantRole, isHideAttendeeNamesEnabled) => { let maskedDisplayName = displayName; if (isHideAttendeeNamesEnabled && participantRole && participantRole === 'Attendee') { if (localUserRole && localUserRole === 'Attendee') { maskedDisplayName = '{AttendeeRole}'; } if (localUserRole && (localUserRole === 'Presenter' || localUserRole === 'Co-organizer' || localUserRole === 'Organizer')) { maskedDisplayName = `{AttendeeRole}(${displayName})`; } } return maskedDisplayName; }; /** * Helper to create a local video stream from the selected camera. * @private */ const createLocalVideoStream = (callClient) => __awaiter$$(void 0, void 0, void 0, function* () { const camera = yield (callClient === null || callClient === void 0 ? void 0 : callClient.getState().deviceManager.selectedCamera); if (camera) { return new communicationCalling.LocalVideoStream(camera); } return undefined; }); /** * Get call state if existing, if not and the call not exists in ended record return undefined, if it never exists, throw an error. * @private */ const getCallStateIfExist = (state, callId) => { if (!state.calls[callId]) { // If call has ended, we don't need to throw an error. if (state.callsEnded[callId]) { return undefined; } throw new Error(`Call Not Found: ${callId}`); } return state.calls[callId]; }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * Selector for {@link MicrophoneButton} component. * * @public */ const microphoneButtonSelector = reselect__namespace.createSelector([getCallExists, getIsMuted, getDeviceManager$1, getCapabilities, getRole$1, getCallState], (callExists, isMuted, deviceManager, capabilities, role, callState) => { const permission = deviceManager.deviceAccess ? deviceManager.deviceAccess.audio : true; const incapable = (capabilities === null || capabilities === void 0 ? void 0 : capabilities.unmuteMic.isPresent) === false && (capabilities === null || capabilities === void 0 ? void 0 : capabilities.unmuteMic.reason) !== 'NotInitialized' || role === 'Consumer'; return { disabled: !callExists || !permission || incapable || callState === 'LocalHold', checked: callExists ? !isMuted : false, microphones: deviceManager.microphones, speakers: deviceManager.speakers, selectedMicrophone: deviceManager.selectedMicrophone, selectedSpeaker: deviceManager.selectedSpeaker }; }); /** * Selector for {@link CameraButton} component. * * @public */ const cameraButtonSelector = reselect__namespace.createSelector([getLocalVideoStreams$1, getDeviceManager$1, getCapabilities, getRole$1, getCallState], (localVideoStreams, deviceManager, capabilities, role, callState) => { const previewOn = _isPreviewOn(deviceManager); const localVideoFromCall = localVideoStreams === null || localVideoStreams === void 0 ? void 0 : localVideoStreams.find(stream => stream.mediaStreamType === 'Video'); const permission = deviceManager.deviceAccess ? deviceManager.deviceAccess.video : true; const incapable = (capabilities === null || capabilities === void 0 ? void 0 : capabilities.turnVideoOn.isPresent) === false && (capabilities === null || capabilities === void 0 ? void 0 : capabilities.turnVideoOn.reason) !== 'NotInitialized' || role === 'Consumer'; return { disabled: !deviceManager.selectedCamera || !permission || !deviceManager.cameras.length || incapable || callState === 'LocalHold', checked: localVideoStreams !== undefined && localVideoStreams.length > 0 ? !!localVideoFromCall : previewOn, cameras: deviceManager.cameras, selectedCamera: deviceManager.selectedCamera }; }); /** * Selector for {@link RaiseHandButton} component. * * @public */ const raiseHandButtonSelector = reselect__namespace.createSelector([getLocalParticipantRaisedHand$1, getCallState], (raisedHand, callState) => { return { checked: raisedHand ? true : false, disabled: callState === 'InLobby' ? true : callState === 'Connecting' }; }); /** * Selector for {@link ReactionButton} component. * * @public */ const reactionButtonSelector = reselect__namespace.createSelector([getLocalParticipantReactionState, getCallState], (reaction, callState) => { return { checked: reaction ? true : false, disabled: callState !== 'Connected' }; }); /** * Selector for {@link ScreenShareButton} component. * * @public */ const screenShareButtonSelector = reselect__namespace.createSelector([getIsScreenSharingOn, getCallState, getCapabilities, getRole$1], (isScreenSharingOn, callState, capabilities, role) => { let disabled = undefined; disabled = disabled || (capabilities === null || capabilities === void 0 ? void 0 : capabilities.shareScreen.isPresent) === false && (capabilities === null || capabilities === void 0 ? void 0 : capabilities.shareScreen.reason) !== 'NotInitialized' || role === 'Consumer' || role === 'Attendee'; disabled = disabled || ['InLobby', 'Connecting', 'LocalHold'].includes(callState !== null && callState !== void 0 ? callState : 'None'); return { checked: isScreenSharingOn, disabled }; }); /** * Selector for {@link DevicesButton} component. * * @public */ const devicesButtonSelector = reselect__namespace.createSelector([getDeviceManager$1], deviceManager => { return { microphones: deviceManager.microphones, speakers: deviceManager.speakers, cameras: deviceManager.cameras, selectedMicrophone: deviceManager.selectedMicrophone, selectedSpeaker: deviceManager.selectedSpeaker, selectedCamera: deviceManager.selectedCamera }; }); /** * Selector for the {@link HoldButton} component. * @public */ const holdButtonSelector = reselect__namespace.createSelector([getCallState], callState => { return { checked: callState === 'LocalHold' }; }); // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __awaiter$_ = (window && window.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * @private */ const areStreamsEqual = (prevStream, newStream) => { return !!prevStream && !!newStream && prevStream.source.id === newStream.source.id; }; /** * Create the common implementation of {@link CallingHandlers} for all types of Call * * @private */ const createDefaultCommonCallingHandlers = memoizeOne((callClient, deviceManager, call, options) => { const onStartLocalVideo = () => __awaiter$_(void 0, void 0, void 0, function* () { // Before the call object creates a stream, dispose of any local preview streams. // @TODO: is there any way to parent the unparented view to the call object instead // of disposing and creating a new stream? yield disposeAllLocalPreviewViews(callClient); const callId = call === null || call === void 0 ? void 0 : call.id; let videoDeviceInfo = callClient.getState().deviceManager.selectedCamera; if (!videoDeviceInfo) { const cameras = yield (deviceManager === null || deviceManager === void 0 ? void 0 : deviceManager.getCameras()); videoDeviceInfo = cameras && cameras.length > 0 ? cameras[0] : undefined; if (videoDeviceInfo) { deviceManager === null || deviceManager === void 0 ? void 0 : deviceManager.selectCamera(videoDeviceInfo); } } if (!callId || !videoDeviceInfo) { return; } const stream = new communicationCalling.LocalVideoStream(videoDeviceInfo); if (call && !call.localVideoStreams.find(s => areStreamsEqual(s, stream))) { yield call.startVideo(stream); } }); const onStopLocalVideo = (stream) => __awaiter$_(void 0, void 0, void 0, function* () { const callId = call === null || call === void 0 ? void 0 : call.id; if (!callId) { return; } if (call && call.localVideoStreams.find(s => areStreamsEqual(s, stream))) { yield call.stopVideo(stream); } }); const onToggleCamera = (options) => __awaiter$_(void 0, void 0, void 0, function* () { const previewOn = _isPreviewOn(callClient.getState().deviceManager); // the disposal of the unparented views is to workaround: https://skype.visualstudio.com/SPOOL/_workitems/edit/3030558. // The root cause of the issue is caused by never transitioning the unparented view to the // call object when going from configuration page (disconnected call state) to connecting. // // Currently the only time the local video stream is moved from unparented view to the call // object is when we transition from connecting -> call state. If the camera was on, // inside the MediaGallery we trigger toggleCamera. This triggers onStartLocalVideo which // destroys the unparentedView and creates a new stream in the call - so all looks well. // // However, if someone turns off their camera during the lobbyOrConnecting screen, the // call.localVideoStreams will be empty (as the stream is currently stored in the unparented // views and was never transitioned to the call object) and thus we incorrectly try to create // a new video stream for the call object, instead of only stopping the unparented view. // // The correct fix for this is to ensure that callAgent.onStartCall is called with the // localvideostream as a videoOption. That will mean call.onLocalVideoStreamsUpdated will // be triggered when the call is in connecting state, which we can then transition the // local video stream to the stateful call client and get into a clean state. if (call && (_isInCall(call.state) || _isInLobbyOrConnecting(call.state))) { const stream = call.localVideoStreams.find(stream => stream.mediaStreamType === 'Video'); const unparentedViews = callClient.getState().deviceManager.unparentedViews; if (stream || unparentedViews.length > 0) { unparentedViews.forEach(view => view.mediaStreamType === 'Video' && callClient.disposeView(undefined, undefined, view)); if (stream) { yield onStopLocalVideo(stream); } } else { yield onStartLocalVideo(); } } else { /** * This will create a unparented view to be used on the configuration page and the connecting screen * * If the device that the stream will come from is not on from permissions checks, then it will take time * to create the stream since device is off. If we are turn the camera on immedietly on the configuration page we see it is * fast but that is because the device is already primed to return a stream. * * On the connecting page the device has already turned off and the connecting window is so small we do not see the resulting * unparented view from the code below. */ const selectedCamera = callClient.getState().deviceManager.selectedCamera; if (selectedCamera) { if (previewOn) { yield onDisposeLocalStreamView(); } else { yield callClient.createView(undefined, undefined, { source: selectedCamera, mediaStreamType: 'Video' }, options); } } } }); const onSelectMicrophone = (device) => __awaiter$_(void 0, void 0, void 0, function* () { if (!deviceManager) { return; } return deviceManager.selectMicrophone(device); }); const onSelectSpeaker = (device) => __awaiter$_(void 0, void 0, void 0, function* () { if (!deviceManager) { return; } return deviceManager.selectSpeaker(device); }); const onSelectCamera = (device, options) => __awaiter$_(void 0, void 0, void 0, function* () { if (!deviceManager) { return; } if (call && _isInCall(call.state)) { deviceManager.selectCamera(device); const stream = call.localVideoStreams.find(stream => stream.mediaStreamType === 'Video'); yield (stream === null || stream === void 0 ? void 0 : stream.switchSource(device)); yield Promise.all([ /// TODO: TEMPORARY SOLUTION /// The Calling SDK needs to wait until the stream is ready before resolving the switchSource promise. /// This is a temporary solution to wait for the stream to be ready before resolving the promise. /// This allows the onSelectCamera to be throttled to prevent the streams from getting in to a frozen state /// if the user switches cameras too rapidly. /// This is to be removed once the Calling SDK has issued a fix. stream === null || stream === void 0 ? void 0 : stream.getMediaStream(), // An extra wait here is also necessary to prevent the remote stream freezing issue. // If this is removed, please test switching cameras rapidly won't cause stream to freeze for remote users. // When this mitigation was introduced, the repro interval time that caused the issue was: // - iPhone 11, safari, v8.3.1: 750ms // - Pixel 6, chrome, Android 15: 400ms // - Windows 11, edge: 100ms new Promise(resolve => setTimeout(resolve, 1000)) ]); } else { const previewOn = _isPreviewOn(callClient.getState().deviceManager); if (!previewOn) { deviceManager.selectCamera(device); return; } yield onDisposeLocalStreamView(); deviceManager.selectCamera(device); yield callClient.createView(undefined, undefined, { source: device, mediaStreamType: 'Video' }, options); } }); const onRaiseHand = () => __awaiter$_(void 0, void 0, void 0, function* () { var _a; return yield ((_a = call === null || call === void 0 ? void 0 : call.feature(communicationCalling.Features.RaiseHand)) === null || _a === void 0 ? void 0 : _a.raiseHand()); }); const onLowerHand = () => __awaiter$_(void 0, void 0, void 0, function* () { var _b; return yield ((_b = call === null || call === void 0 ? void 0 : call.feature(communicationCalling.Features.RaiseHand)) === null || _b === void 0 ? void 0 : _b.lowerHand()); }); const onToggleRaiseHand = () => __awaiter$_(void 0, void 0, void 0, function* () { const raiseHandFeature = call === null || call === void 0 ? void 0 : call.feature(communicationCalling.Features.RaiseHand); const localUserId = callClient.getState().userId; const isLocalRaisedHand = raiseHandFeature === null || raiseHandFeature === void 0 ? void 0 : raiseHandFeature.getRaisedHands().find(publishedState => toFlatCommunicationIdentifier(publishedState.identifier) === toFlatCommunicationIdentifier(localUserId)); if (isLocalRaisedHand) { yield (raiseHandFeature === null || raiseHandFeature === void 0 ? void 0 : raiseHandFeature.lowerHand()); } else { yield (raiseHandFeature === null || raiseHandFeature === void 0 ? void 0 : raiseHandFeature.raiseHand()); } }); const onReactionClick = (reaction) => __awaiter$_(void 0, void 0, void 0, function* () { var _c; if (reaction === 'like' || reaction === 'applause' || reaction === 'heart' || reaction === 'laugh' || reaction === 'surprised') { yield ((_c = call === null || call === void 0 ? void 0 : call.feature(communicationCalling.Features.Reaction)) === null || _c === void 0 ? void 0 : _c.sendReaction({ reactionType: reaction })); } else { console.warn(`Can not recognize ${reaction} as meeting reaction`); } return; }); const onToggleMicrophone = () => __awaiter$_(void 0, void 0, void 0, function* () { if (!call || !(_isInCall(call.state) || _isInLobbyOrConnecting(call.state))) { throw new Error(`Please invoke onToggleMicrophone after call is started`); } return call.isMuted ? yield call.unmute() : yield call.mute(); }); const onStartScreenShare = () => __awaiter$_(void 0, void 0, void 0, function* () { return yield (call === null || call === void 0 ? void 0 : call.startScreenSharing()); }); const onStopScreenShare = () => __awaiter$_(void 0, void 0, void 0, function* () { return yield (call === null || call === void 0 ? void 0 : call.stopScreenSharing()); }); const onToggleScreenShare = () => __awaiter$_(void 0, void 0, void 0, function* () { return (call === null || call === void 0 ? void 0 : call.isScreenSharingOn) ? yield onStopScreenShare() : yield onStartScreenShare(); }); const onHangUp = (forEveryone) => __awaiter$_(void 0, void 0, void 0, function* () { return yield (call === null || call === void 0 ? void 0 : call.hangUp({ forEveryone: forEveryone === true ? true : false })); }); const onToggleHold = () => __awaiter$_(void 0, void 0, void 0, function* () { return (call === null || call === void 0 ? void 0 : call.state) === 'LocalHold' ? yield (call === null || call === void 0 ? void 0 : call.resume()) : yield (call === null || call === void 0 ? void 0 : call.hold()); }); const onCreateLocalStreamView = (...args_1) => __awaiter$_(void 0, [...args_1], void 0, function* (options = { scalingMode: 'Crop', isMirrored: true }) { if (!call || call.localVideoStreams.length === 0) { return; } const callState = callClient.getState().calls[call.id]; if (!callState) { return; } const localStream = callState.localVideoStreams.find(item => item.mediaStreamType === 'Video'); const localScreenSharingStream = callState.localVideoStreams.find(item => item.mediaStreamType === 'ScreenSharing'); let createViewResult = undefined; if (localStream && !localStream.view) { createViewResult = yield callClient.createView(call.id, undefined, localStream, options); } if (localScreenSharingStream && !localScreenSharingStream.view && call.isScreenSharingOn) { // Hardcoded `scalingMode` since it is highly unlikely that CONTOSO would ever want to use a different scaling mode for screenshare. // Using `Crop` would crop the contents of screenshare and `Stretch` would warp it. // `Fit` is the only mode that maintains the integrity of the screen being shared. createViewResult = yield callClient.createView(call.id, undefined, localScreenSharingStream, { scalingMode: 'Fit' }); } return (createViewResult === null || createViewResult === void 0 ? void 0 : createViewResult.view) ? { view: createViewResult === null || createViewResult === void 0 ? void 0 : createViewResult.view } : undefined; }); const onCreateRemoteStreamView = (userId_1, ...args_2) => __awaiter$_(void 0, [userId_1, ...args_2], void 0, function* (userId, options = { scalingMode: 'Crop' }) { if (!call) { return; } const callState = getCallStateIfExist(callClient.getState(), call.id); if (!callState) { return; } const participant = Object.values(callState.remoteParticipants).find(participant => toFlatCommunicationIdentifier(participant.identifier) === userId); if (!participant || !participant.videoStreams) { return; } /** * There is a bug from the calling sdk where if a user leaves and rejoins immediately * it adds 2 more potential streams this remote participant can use. The old 2 streams * still show as available and that is how we got a frozen stream in this case. The stopgap * until streams accurately reflect their availability is to always prioritize the latest streams of a certain type * e.g findLast instead of find */ // Find the first available stream, if there is none, then get the first stream const remoteVideoStream = Object.values(participant.videoStreams).findLast(i => i.mediaStreamType === 'Video' && i.isAvailable) || Object.values(participant.videoStreams).findLast(i => i.mediaStreamType === 'Video'); const screenShareStream = Object.values(participant.videoStreams).findLast(i => i.mediaStreamType === 'ScreenSharing' && i.isAvailable) || Object.values(participant.videoStreams).findLast(i => i.mediaStreamType === 'ScreenSharing'); let createViewResult = undefined; if (remoteVideoStream && remoteVideoStream.isAvailable && !remoteVideoStream.view) { createViewResult = yield callClient.createView(call.id, participant.identifier, remoteVideoStream, options); } if (screenShareStream && screenShareStream.isAvailable && !screenShareStream.view) { // Hardcoded `scalingMode` since it is highly unlikely that CONTOSO would ever want to use a different scaling mode for screenshare. // Using `Crop` would crop the contents of screenshare and `Stretch` would warp it. // `Fit` is the only mode that maintains the integrity of the screen being shared. createViewResult = yield callClient.createView(call.id, participant.identifier, screenShareStream, { scalingMode: 'Fit' }); } return (createViewResult === null || createViewResult === void 0 ? void 0 : createViewResult.view) ? { view: createViewResult === null || createViewResult === void 0 ? void 0 : createViewResult.view } : undefined; }); const onDisposeRemoteStreamView = (userId) => __awaiter$_(void 0, void 0, void 0, function* () { if (!call) { return; } const callState = getCallStateIfExist(callClient.getState(), call.id); if (!callState) { return; } const participant = Object.values(callState.remoteParticipants).find(participant => toFlatCommunicationIdentifier(participant.identifier) === userId); if (!participant || !participant.videoStreams) { return; } const remoteVideoStream = Object.values(participant.videoStreams).find(i => i.mediaStreamType === 'Video'); const screenShareStream = Object.values(participant.videoStreams).find(i => i.mediaStreamType === 'ScreenSharing'); if (remoteVideoStream && remoteVideoStream.view) { callClient.disposeView(call.id, participant.identifier, remoteVideoStream); } if (screenShareStream && screenShareStream.view) { callClient.disposeView(call.id, participant.identifier, screenShareStream); } }); const onDisposeRemoteVideoStreamView = (userId) => __awaiter$_(void 0, void 0, void 0, function* () { if (!call) { return; } const callState = getCallStateIfExist(callClient.getState(), call.id); if (!callState) { return; } const participant = Object.values(callState.remoteParticipants).find(participant => toFlatCommunicationIdentifier(participant.identifier) === userId); if (!participant || !participant.videoStreams) { return; } const remoteVideoStream = Object.values(participant.videoStreams).filter(i => i.mediaStreamType === 'Video'); for (const stream of remoteVideoStream) { if (stream.view) { callClient.disposeView(call.id, participant.identifier, stream); } } }); const onDisposeRemoteScreenShareStreamView = (userId) => __awaiter$_(void 0, void 0, void 0, function* () { if (!call) { return; } const callState = getCallStateIfExist(callClient.getState(), call.id); if (!callState) { return; } const participant = Object.values(callState.remoteParticipants).find(participant => toFlatCommunicationIdentifier(participant.identifier) === userId); if (!participant || !participant.videoStreams) { return; } const screenShareStreams = Object.values(participant.videoStreams).filter(i => i.mediaStreamType === 'ScreenSharing'); for (const stream of screenShareStreams) { if (stream.view) { callClient.dispose