diff --git a/change-beta/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json b/change-beta/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json new file mode 100644 index 00000000000..89cb2416162 --- /dev/null +++ b/change-beta/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "feature", + "workstream": "", + "comment": "Support Composites joining existing calls by updating CallAdapter and CallWithChatAdapters to take an existing call as a contruction parameter instead of a locator.", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json b/change/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json new file mode 100644 index 00000000000..89cb2416162 --- /dev/null +++ b/change/@azure-communication-react-424e27fb-3ea9-4505-8fb2-2d7c0c28cd2c.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "feature", + "workstream": "", + "comment": "Support Composites joining existing calls by updating CallAdapter and CallWithChatAdapters to take an existing call as a contruction parameter instead of a locator.", + "packageName": "@azure/communication-react", + "email": "2684369+JamesBurnside@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/calling-stateful-client/src/TypeGuards.ts b/packages/calling-stateful-client/src/TypeGuards.ts index 5100284bd6f..5962371c293 100644 --- a/packages/calling-stateful-client/src/TypeGuards.ts +++ b/packages/calling-stateful-client/src/TypeGuards.ts @@ -3,6 +3,7 @@ import { Call, CallAgent, TeamsCallAgent, IncomingCallCommon } from '@azure/communication-calling'; import { CallAgentCommon, CallCommon, TeamsCall } from './BetaToStableTypes'; +import { CallState } from './CallClientState'; /** * @internal @@ -47,8 +48,16 @@ export const _isACSIncomingCall = (call: IncomingCallCommon): boolean => { }; /** + * Heuristic to detect if a call is a Teams meeting call. + * `threadId` is only available when the call is connected. + * `InLobby` state is only available for Teams calls currently. + * + * @remarks + * This is a heuristic is not accurate when the call is in the connecting or earlymedia states. + * If ACS group calls or rooms calls support threadId or InLobby state, this heuristic will need to be updated. + * * @internal */ -export const _isTeamsMeeting = (call: CallCommon): boolean => { - return 'info' in call && !!call.info.threadId; +export const _isTeamsMeeting = (call: CallCommon | CallState): boolean => { + return ('info' in call && !!call.info?.threadId) || call.state === 'InLobby'; }; diff --git a/packages/calling-stateful-client/src/index.ts b/packages/calling-stateful-client/src/index.ts index e3d201480ba..6c310af3839 100644 --- a/packages/calling-stateful-client/src/index.ts +++ b/packages/calling-stateful-client/src/index.ts @@ -5,6 +5,6 @@ export * from './index-public'; export { _createStatefulCallClientInner } from './StatefulCallClient'; -export { _isACSCall, _isACSCallAgent, _isTeamsCall, _isTeamsCallAgent } from './TypeGuards'; +export { _isACSCall, _isACSCallAgent, _isTeamsCall, _isTeamsCallAgent, _isTeamsMeeting } from './TypeGuards'; export type { CallAgentCommon, CallCommon, TeamsCall } from './BetaToStableTypes'; diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 8f334905a47..6f66feab8bb 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -2630,11 +2630,24 @@ export function createAzureCommunicationCallAdapterFromClient(callClient: Statef // @public export function createAzureCommunicationCallAdapterFromClient(callClient: StatefulCallClient, callAgent: CallAgent, locator: CallAdapterLocator, options?: AzureCommunicationCallAdapterOptions): Promise; +// @public +export function createAzureCommunicationCallAdapterFromClient(callClient: StatefulCallClient, callAgent: CallAgent, call: Call, options?: AzureCommunicationCallAdapterOptions): Promise; + // @public export const createAzureCommunicationCallWithChatAdapter: ({ userId, displayName, credential, endpoint, locator, alternateCallerId, callAdapterOptions }: AzureCommunicationCallWithChatAdapterArgs) => Promise; // @public -export const createAzureCommunicationCallWithChatAdapterFromClients: ({ callClient, callAgent, callLocator, chatClient, chatThreadClient, callAdapterOptions }: AzureCommunicationCallWithChatAdapterFromClientArgs) => Promise; +export function createAzureCommunicationCallWithChatAdapterFromClients(args: AzureCommunicationCallWithChatAdapterFromClientArgs): Promise; + +// @public +export function createAzureCommunicationCallWithChatAdapterFromClients(args: { + callClient: StatefulCallClient; + callAgent: CallAgent; + call: Call; + chatClient: StatefulChatClient; + chatThreadClient: ChatThreadClient; + callAdapterOptions?: AzureCommunicationCallAdapterOptions; +}): Promise; // @public export const createAzureCommunicationChatAdapter: ({ endpoint: endpointUrl, userId, displayName, credential, threadId }: AzureCommunicationChatAdapterArgs) => Promise; diff --git a/packages/communication-react/review/stable/communication-react.api.md b/packages/communication-react/review/stable/communication-react.api.md index 0103a11aeaa..d471fa9e2ed 100644 --- a/packages/communication-react/review/stable/communication-react.api.md +++ b/packages/communication-react/review/stable/communication-react.api.md @@ -2260,11 +2260,24 @@ export function createAzureCommunicationCallAdapterFromClient(callClient: Statef // @public export function createAzureCommunicationCallAdapterFromClient(callClient: StatefulCallClient, callAgent: CallAgent, locator: CallAdapterLocator, options?: AzureCommunicationCallAdapterOptions): Promise; +// @public +export function createAzureCommunicationCallAdapterFromClient(callClient: StatefulCallClient, callAgent: CallAgent, call: Call, options?: AzureCommunicationCallAdapterOptions): Promise; + // @public export const createAzureCommunicationCallWithChatAdapter: ({ userId, displayName, credential, endpoint, locator, alternateCallerId, callAdapterOptions }: AzureCommunicationCallWithChatAdapterArgs) => Promise; // @public -export const createAzureCommunicationCallWithChatAdapterFromClients: ({ callClient, callAgent, callLocator, chatClient, chatThreadClient, callAdapterOptions }: AzureCommunicationCallWithChatAdapterFromClientArgs) => Promise; +export function createAzureCommunicationCallWithChatAdapterFromClients(args: AzureCommunicationCallWithChatAdapterFromClientArgs): Promise; + +// @public +export function createAzureCommunicationCallWithChatAdapterFromClients(args: { + callClient: StatefulCallClient; + callAgent: CallAgent; + call: Call; + chatClient: StatefulChatClient; + chatThreadClient: ChatThreadClient; + callAdapterOptions?: AzureCommunicationCallAdapterOptions; +}): Promise; // @public export const createAzureCommunicationChatAdapter: ({ endpoint: endpointUrl, userId, displayName, credential, threadId }: AzureCommunicationChatAdapterArgs) => Promise; diff --git a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts index c7c78a8c438..ea96332557b 100644 --- a/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts @@ -11,8 +11,7 @@ import { StatefulCallClient, StatefulDeviceManager, TeamsCall, - _isACSCall, - _isTeamsCall + _isACSCall } from '@internal/calling-stateful-client'; import { AcceptedTransfer } from '@internal/calling-stateful-client'; import { _isTeamsCallAgent } from '@internal/calling-stateful-client'; @@ -86,7 +85,16 @@ import { VideoBackgroundReplacementEffect } from './CallAdapter'; import { TeamsCallAdapter } from './CallAdapter'; -import { getCallCompositePage, getLocatorOrTargetCallees, IsCallEndedPage, isCameraOn } from '../utils'; +import { + getCallCompositePage, + isCall, + IsCallEndedPage, + isCameraOn, + isDetectedAsRoomsCall, + isDetectedAsTeamsCallKind, + isDetectedAsTeamsMeeting, + isTargetCallees +} from '../utils'; import { CreateVideoStreamViewResult, VideoStreamOptions } from '@internal/react-components'; import { toFlatCommunicationIdentifier, _toCommunicationIdentifier, _isValidIdentifier } from '@internal/acs-ui-common'; import { @@ -96,8 +104,7 @@ import { MicrosoftTeamsUserIdentifier, isMicrosoftTeamsUserIdentifier, MicrosoftTeamsAppIdentifier, - UnknownIdentifier, - isMicrosoftTeamsAppIdentifier + UnknownIdentifier } from '@azure/communication-common'; import { isCommunicationUserIdentifier } from '@azure/communication-common'; import { isPhoneNumberIdentifier, PhoneNumberIdentifier } from '@azure/communication-common'; @@ -139,13 +146,12 @@ class CallContext { private emitter: EventEmitter = new EventEmitter(); private state: CallContextState; private callId: string | undefined; + private locator: CallAdapterLocator | undefined; private displayNameModifier: AdapterStateModifier | undefined; constructor( clientState: CallClientState, - isTeamsCall: boolean, - isTeamsMeeting: boolean, - isRoomsCall: boolean, + locator: CallAdapterLocator | undefined, options?: { maxListeners?: number; onFetchProfile?: OnFetchProfileCallback; @@ -164,19 +170,20 @@ class CallContext { }, targetCallees?: StartCallIdentifier[] ) { + this.locator = locator; this.state = { isLocalPreviewMicrophoneEnabled: false, userId: clientState.userId, displayName: clientState.callAgent?.displayName, devices: clientState.deviceManager, call: undefined, + isTeamsMeeting: isDetectedAsTeamsMeeting(locator, undefined), + isTeamsCall: isDetectedAsTeamsCallKind(targetCallees, undefined), + isRoomsCall: isDetectedAsRoomsCall(locator, undefined), targetCallees: targetCallees as CommunicationIdentifier[], page: 'configuration', latestErrors: clientState.latestErrors, /* @conditional-compile-remove(breakout-rooms) */ latestNotifications: clientState.latestNotifications, - isTeamsCall, - isTeamsMeeting, - isRoomsCall, alternateCallerId: options?.alternateCallerId, environmentInfo: clientState.environmentInfo, /* @conditional-compile-remove(unsupported-browser) */ unsupportedBrowserVersionsAllowed: false, @@ -269,6 +276,7 @@ class CallContext { environmentInfo: this.state.environmentInfo, unsupportedBrowserVersionOptedIn: this.state.unsupportedBrowserVersionsAllowed }; + const targetCallees = this.state.targetCallees; const latestAcceptedTransfer = call?.transfer.acceptedTransfers ? findLatestAcceptedTransfer(call.transfer.acceptedTransfers) @@ -313,7 +321,10 @@ class CallContext { clientState.deviceManager.unparentedViews.find((s) => s.mediaStreamType === 'Video') ? 'On' : 'Off', - acceptedTransferCallState: transferCall + acceptedTransferCallState: transferCall, + isTeamsMeeting: isDetectedAsTeamsMeeting(this.locator, call), + isTeamsCall: isDetectedAsTeamsCallKind(targetCallees, call), + isRoomsCall: isDetectedAsRoomsCall(this.locator, call) }); } } @@ -420,7 +431,14 @@ export class AzureCommunicationCallAdapter { - if (isMicrosoftTeamsUserIdentifier(callee) || isMicrosoftTeamsAppIdentifier(callee)) { - isTeamsCall = true; - } - }); - const isRoomsCall = this.locator ? 'roomId' in this.locator : false; + let overloadedParamAsCall: Call | undefined = undefined; + if (isTargetCallees(overloadedParam)) { + this.targetCallees = overloadedParam; + } else if (isCall(overloadedParam)) { + overloadedParamAsCall = overloadedParam; + } else { + this.locator = overloadedParam; + } + + this.deviceManager = deviceManager; this.onResolveVideoBackgroundEffectsDependency = options?.videoBackgroundOptions?.onResolveDependency; this.onResolveDeepNoiseSuppressionDependency = options?.deepNoiseSuppressionOptions?.onResolveDependency; - this.context = new CallContext( - callClient.getState(), - !!isTeamsCall, - isTeamsMeeting, - isRoomsCall, - options, - this.targetCallees - ); + this.context = new CallContext(callClient.getState(), this.locator, options, this.targetCallees); this.context.onCallEnded((endCallData) => this.emitter.emit('callEnded', endCallData)); @@ -560,6 +564,10 @@ export class AzureCommunicationCallAdapter; +/** + * Implementation of overloads for {@link createAzureCommunicationCallAdapterFromClient}. + * + * @private + */ +export async function createAzureCommunicationCallAdapterFromClient( + callClient: StatefulCallClient, + callAgent: CallAgent, + overloadedParam: CallAdapterLocator | StartCallIdentifier[] | Call, options?: AzureCommunicationCallAdapterOptions ): Promise { const deviceManager = (await callClient.getDeviceManager()) as StatefulDeviceManager; @@ -2183,22 +2202,14 @@ export async function createAzureCommunicationCallAdapterFromClient( if (deviceManager.isSpeakerSelectionAvailable) { await deviceManager.getSpeakers(); } - if (getLocatorOrTargetCallees(locatorOrtargetCallees)) { - return new AzureCommunicationCallAdapter( - callClient, - locatorOrtargetCallees as StartCallIdentifier[], - callAgent, - deviceManager, - options - ); + /* @conditional-compile-remove(unsupported-browser) */ + await callClient.feature(Features.DebugInfo).getEnvironmentInfo(); + if (isTargetCallees(overloadedParam)) { + return new AzureCommunicationCallAdapter(callClient, overloadedParam, callAgent, deviceManager, options); + } else if (isCall(overloadedParam)) { + return new AzureCommunicationCallAdapter(callClient, overloadedParam, callAgent, deviceManager, options); } else { - return new AzureCommunicationCallAdapter( - callClient, - locatorOrtargetCallees as CallAdapterLocator, - callAgent, - deviceManager, - options - ); + return new AzureCommunicationCallAdapter(callClient, overloadedParam, callAgent, deviceManager, options); } } diff --git a/packages/react-composites/src/composites/CallComposite/utils/Utils.ts b/packages/react-composites/src/composites/CallComposite/utils/Utils.ts index 94788073f95..cdef15e3e8e 100644 --- a/packages/react-composites/src/composites/CallComposite/utils/Utils.ts +++ b/packages/react-composites/src/composites/CallComposite/utils/Utils.ts @@ -4,8 +4,13 @@ import { CallAdapterState, CallCompositePage, END_CALL_PAGES, StartCallIdentifier } from '../adapter/CallAdapter'; import { _isInCall, _isPreviewOn, _isInLobbyOrConnecting } from '@internal/calling-component-bindings'; import { CallControlOptions } from '../types/CallControlOptions'; -import { CallState, RemoteParticipantState } from '@internal/calling-stateful-client'; -import { isPhoneNumberIdentifier } from '@azure/communication-common'; +import { CallState, RemoteParticipantState, _isTeamsMeeting } from '@internal/calling-stateful-client'; +import { + CommunicationIdentifier, + isMicrosoftTeamsAppIdentifier, + isMicrosoftTeamsUserIdentifier, + isPhoneNumberIdentifier +} from '@azure/communication-common'; import { AdapterStateModifier, CallAdapterLocator } from '../adapter/AzureCommunicationCallAdapter'; import { VideoBackgroundEffectsDependency } from '@internal/calling-component-bindings'; @@ -13,7 +18,7 @@ import { VideoBackgroundEffectsDependency } from '@internal/calling-component-bi import { VideoBackgroundEffect } from '../adapter/CallAdapter'; import { EnvironmentInfo, VideoDeviceInfo } from '@azure/communication-calling'; -import { VideoEffectProcessor } from '@azure/communication-calling'; +import { Call, VideoEffectProcessor } from '@azure/communication-calling'; import { CompositeLocale } from '../../localization'; import { CallCompositeIcons } from '../../common/icons'; @@ -253,6 +258,38 @@ export const getEndedCallPageProps = ( return { title, moreDetails, disableStartCallButton, iconName }; }; +/** @private */ +export const isDetectedAsTeamsMeeting = ( + locator: CallAdapterLocator | undefined, + call: CallState | undefined +): boolean => { + const locatorIsTeamsMeeting = locator && ('meetingLink' in locator || 'meetingId' in locator); + const callIsTeamsMeeting = call && _isTeamsMeeting(call); + return !!locatorIsTeamsMeeting || !!callIsTeamsMeeting; +}; + +/** @private */ +export const isDetectedAsRoomsCall = ( + locator: CallAdapterLocator | undefined, + call: CallState | undefined +): boolean => { + return !!(locator && 'roomId' in locator) || !!(call?.info && 'roomId' in call.info); +}; + +/** @private */ +export const isDetectedAsTeamsCallKind = ( + targetCallees: CommunicationIdentifier[] | undefined, + call?: CallState | undefined +): boolean => { + let isTeamsCall: boolean = call?.kind === 'TeamsCall'; + targetCallees?.forEach((callee) => { + if (isMicrosoftTeamsUserIdentifier(callee) || isMicrosoftTeamsAppIdentifier(callee)) { + isTeamsCall = true; + } + }); + return isTeamsCall; +}; + /** * type definition for conditional-compilation */ @@ -582,14 +619,18 @@ export const getSelectedCameraFromAdapterState = (state: CallAdapterState): Vide /** * Helper to determine if the adapter has a locator or targetCallees - * @param locatorOrTargetCallees * @returns boolean to determine if the adapter has a locator or targetCallees, true is locator, false is targetCallees * @private */ -export const getLocatorOrTargetCallees = ( - locatorOrTargetCallees: CallAdapterLocator | StartCallIdentifier[] -): locatorOrTargetCallees is StartCallIdentifier[] => { - return !!Array.isArray(locatorOrTargetCallees); +export const isTargetCallees = ( + overloadedParam: CallAdapterLocator | StartCallIdentifier[] | Call +): overloadedParam is StartCallIdentifier[] => { + return !!Array.isArray(overloadedParam); +}; + +/** @private */ +export const isCall = (overloadedParam: CallAdapterLocator | StartCallIdentifier[] | Call): overloadedParam is Call => { + return 'kind' in overloadedParam && overloadedParam.kind === 'Call'; }; /** diff --git a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts index cbf7bdfd592..df84e7488c9 100644 --- a/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts +++ b/packages/react-composites/src/composites/CallWithChatComposite/adapter/AzureCommunicationCallWithChatAdapter.ts @@ -1379,7 +1379,6 @@ export type AzureCommunicationCallWithChatAdapterFromClientArgs = { callClient: StatefulCallClient; chatClient: StatefulChatClient; chatThreadClient: ChatThreadClient; - callAdapterOptions?: AzureCommunicationCallAdapterOptions; }; @@ -1392,24 +1391,59 @@ export type AzureCommunicationCallWithChatAdapterFromClientArgs = { * * @public */ -export const createAzureCommunicationCallWithChatAdapterFromClients = async ({ - callClient, - callAgent, - callLocator, - chatClient, - chatThreadClient, - callAdapterOptions -}: AzureCommunicationCallWithChatAdapterFromClientArgs): Promise => { - const callAdapter = await createAzureCommunicationCallAdapterFromClient( - callClient, - callAgent, - callLocator, +export async function createAzureCommunicationCallWithChatAdapterFromClients( + args: AzureCommunicationCallWithChatAdapterFromClientArgs +): Promise; + +/** + * Create a {@link CallWithChatAdapter} using the provided {@link StatefulChatClient} and {@link StatefulCallClient} and {@link Call}. + * + * Useful if you want to keep a reference to {@link StatefulChatClient} and {@link StatefulCallClient}. + * Please note that chatThreadClient has to be created by StatefulChatClient via chatClient.getChatThreadClient(chatThreadId) API. + * Consider using {@link createAzureCommunicationCallWithChatAdapter} for a simpler API. + * + * @public + */ +export async function createAzureCommunicationCallWithChatAdapterFromClients(args: { + callClient: StatefulCallClient; + callAgent: CallAgent; + call: Call; + chatClient: StatefulChatClient; + chatThreadClient: ChatThreadClient; + callAdapterOptions?: AzureCommunicationCallAdapterOptions; +}): Promise; + +/** + * Implementation of {@link createAzureCommunicationCallWithChatAdapterFromClients} overloads. + * @private + */ +export async function createAzureCommunicationCallWithChatAdapterFromClients( + args: + | AzureCommunicationCallWithChatAdapterFromClientArgs + | { + call: Call; + callAgent: CallAgent; + callClient: StatefulCallClient; + chatClient: StatefulChatClient; + chatThreadClient: ChatThreadClient; + callAdapterOptions?: AzureCommunicationCallAdapterOptions; + } +): Promise { + const { callAgent, callClient, chatClient, chatThreadClient, callAdapterOptions } = args; + + const callAdapter = + 'call' in args + ? await createAzureCommunicationCallAdapterFromClient(callClient, callAgent, args.call, callAdapterOptions) + : await createAzureCommunicationCallAdapterFromClient( + callClient, + callAgent, + args.callLocator, + callAdapterOptions + ); - callAdapterOptions - ); const chatAdapter = await createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient); return new AzureCommunicationCallWithChatAdapter(callAdapter, chatAdapter); -}; +} /** * Create a {@link CallWithChatAdapter} from the underlying adapters.